I previously wrote about how I could ConsecutiveScroller on GitHub open source on a sliding control that enabled layout top-down functionality. Interested friends can go to have a look at: Android slide layout ConsecutiveScrollerLayout layout top absorption function. ConsecutiveScrollerLayout are introduced in this paper is how the layout of the sliding distance by calculation, to suck the top view set y offset, make it hover at the top. But when the view hovers over the top, it overrides the view behind it. This is due to the display hierarchy of the Android layout. When two views overlap, the one added later overwrites the one added first. What we want is that when the top view overlaps with other views, the top view is displayed on the top layer, covering the view behind it. My solution at the time was to make the top view translationZ so that its display layer is higher than the other views so that it is not overwritten by other views. This is a good solution to the problem of overlapping views, however translationZ is supported only in Android 5.0 and cannot be used on phones below 5.0. This made the top absorption function ConsecutiveScrollerLayout can only use in Android 5.0 more mobile phone, which greatly limits its scope of application. If our project is under 5.0, there is no way we can make the top suction feature only work on phones over 5.0 and not under 5.0. So I needed to find a way to make it work on phones below 5.0.

To analyze problems

The setTranslationZ() method is 5.0. The setTranslationZ() method is 5.0. Does Android provide backward compatibility? So I found ViewCompat setTranslationZ () method.

    public static void setTranslationZ(@NonNull View view, float translationZ) {
        if (VERSION.SDK_INT >= 21) { view.setTranslationZ(translationZ); }}Copy the code

What a disappointment, it just judged the following version so that no errors were reported below 5.0, but it did nothing. Since Android itself doesn’t handle anything below 5.0, it’s obviously impossible to make the View z-axis compatible downward.

Back to the problem itself, we want the top view to be at the top of the screen, not overwritten by other views. All the content displayed on the Android interface is drawn on a Canvas. If the same area is drawn more than once, the content drawn first is overwritten by the content drawn later. Views are drawn first, then drawn, so when views overlap, the view behind will overwrite the view in front. As long as the top view is drawn after other views, the top view will be displayed on top of other views and will not be overwritten by other views. So is there a way to make sure that the top view is drawn last? The easiest and most straightforward way is to let the top view be added last, but the problem is that the order in which the views are added affects not only the order in which they are drawn, but also the order in which they are arranged and displayed. What we want is to change the order in which the view is drawn, and not change the position of the view. So this method is obviously not good either. Is there a way to change the order in which a view is drawn without changing the order in which it is added? We know that layout traverses its sub-view and distributes the process of measurement, layout and drawing in the process of measure, layout and draw. If we change the order of the subviews before layout DRAW and restore them after draw, then we are guaranteed to change only the drawing order of the views.

Solution 1.0

The child views of a ViewGroup are stored in the mChildren array.

private View[] mChildren;
Copy the code

Since it is private, fetching and modifying it requires reflection.

/ / get mChildren
private View[] getChildren() {
    try {
        Class aClass = Class.forName("android.view.ViewGroup");
        Field field = aClass.getDeclaredField("mChildren");
        field.setAccessible(true); 
        Object resultValue = field.get(this);
        return (View[]) resultValue;
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}
/ / set mChildren
private void setChildren(View[] children) {
    try {
        Class aClass = Class.forName("android.view.ViewGroup");
        Field field = aClass.getDeclaredField("mChildren");
        field.setAccessible(true); // Private attributes must be set for access
        field.set(this, children);
    } catch(Exception e) { e.printStackTrace(); }}Copy the code

Before drawing, modify the order of view, after drawing restore.

// A temporary variable to save the original array of mChildren
private View[] tempViews = null;

@Override
public void draw(Canvas canvas) {
   // It is compatible with top suction functions below 5.0
   if(Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP && ! getStickyChildren().isEmpty()) { tempViews = getChildren();if(tempViews ! =null) {
         / / modify mChildrensetChildren(sortViews(tempViews.length)); }}super.draw(canvas);

   // It is compatible with top suction functions below 5.0
   if(Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP && ! getStickyChildren().isEmpty() && tempViews ! =null) {
     / / mChildren recoverysetChildren(tempViews); }}// Returns the sorted children array
private View[] sortViews(int size) {
    View[] views = new View[size];
    int index = 0;
    int count = getChildCount();
    for (int i = 0; i < count; i++) {
        View child = getChildAt(i);
        / / common view
        if (!isStickyChild(child)) {
            views[index] = child;
            index++;
        }
    }

    for (int i = 0; i < count; i++) {
        View child = getChildAt(i);
        / / the top view
        if(isStickyChild(child)) { views[index] = child; index++; }}return views;
}
Copy the code

Run the test. When the view is on the top, it will display normally on the top and will not be overwritten by the view below, as if the problem has been solved perfectly. But when I click the control on the interface, a new problem appears, I click the view and the response view is not the same, the transfer of the event is messed up. Because we have changed the order in which the views are drawn, the view we actually see and operate on May not be the same view that the system judges. Obviously, this solution raises new problems and is not desirable.

Analysis of the source code

Since modifying mChildren didn’t work, we had to find another solution. I tried to track the view drawing source code, looking forward to some new ideas. Draw() –>dispatchDraw(); The dispatchDraw() method in ViewGroup is the key code for drawing child Views. By reading the source code, I found a few key lines of code.

    @Override
    protected void dispatchDraw(Canvas canvas) {
        
				// Step 1: Get the predefined sort list
        final ArrayList<View> preorderedList = usingRenderNodeProperties
                ? null : buildOrderedChildList();
				
				// Step 2: Determine whether a custom sort is required
        final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled();
				
        for (int i = 0; i < childrenCount; i++) {
						// Step 3: Obtain view subscripts according to the drawing order
            final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
						// Step 4: Get subviews according to subscripts
            final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
            if((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() ! =null) {
								// Step 5: Draw a subviewmore |= drawChild(canvas, child, drawingTime); }}}Copy the code

Step 1: Get the predefined sort list. If open the hardware acceleration usingRenderNodeProperties to true, preorderedList is null. Otherwise, call buildOrderedChildList(), which mostly returns null, so preorderedList is always null. The buildOrderedChildList() method does not return NULL only if hardware acceleration is not set and z-axis height is set for the child view. The buildOrderedChildList() method will not return null if hardware acceleration is disabled after 5.0 and the z-axis of the child view is set. The buildOrderedChildList() method will not return null if hardware acceleration is disabled after 5.0 and the z-axis is set. This method handles this situation, and its sorting of views is basically the same as the logic we analyze below, so we can ignore this method. Step 2: Determine whether a custom sort is required. Since preorderedList is null, so whether you need a custom sort of judgment is isChildrenDrawingOrderEnabled () method, this method is the default is false, only is set to true, only effective custom sorting, this is we need to focus on the first method. Step 3: Get the view subscript according to the drawing order. Look directly at the code:

    private int getAndVerifyPreorderedIndex(int childrenCount, int i, boolean customOrder) {
        final int childIndex;
        if (customOrder) {
          // If you customize the sort, get the view subscripts according to the order
            final int childIndex1 = getChildDrawingOrder(childrenCount, i);
            if (childIndex1 >= childrenCount) {
                throw new IndexOutOfBoundsException("getChildDrawingOrder() "
                        + "returned invalid index " + childIndex1
                        + " (child count is " + childrenCount + ")");
            }
            childIndex = childIndex1;
        } else {
          // Not custom sort, subscript and order are the same
            childIndex = i;
        }
        return childIndex;
    }
Copy the code

In this method, if not sorted, the subscripts are returned in the same order, so the default drawing order is the order in which the view is added. If sorting is required, getChildDrawingOrder is used to get the subscripts of the views to be drawn in the order that is determined by the return value of this method.

protected int getChildDrawingOrder(int childCount, int drawingPosition) {
    return drawingPosition;
}
Copy the code

As you can see, this method still returns the order itself, so its default drawing order is also the order in which views are added. But this method is protected, which means we can override this method, return the index we want, and change the order in which the view is drawn. This is the second approach we need to focus on.

Step 4: according to the index, called getAndVerifyPreorderedView or need to draw the 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;
    }
Copy the code

It’s very simple, just by subscript or view, if you have a predefined sort, you get it from the preorderedList, otherwise you get it from the Children array, which is an array of subviews, in order of addition.

DrawChild: call child’s draw method to draw a child view.

Finally realize

Now we know that to change the drawing order of a ViewGroup’s child views, we can simply turn on custom sorting and override getChildDrawingOrder.

Call in the constructor of a custom ViewGroup:

// Enable custom sorting
setChildrenDrawingOrderEnabled(true);
Copy the code

Preprocess the sorting of views

// Save the pre-processed sort
private final List<View> mViews = new ArrayList<>();

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
  
  // Ignore other code
  
  	/ / sorting
    sortViews();
}

private void sortViews(a) {
    List<View> list = new ArrayList<>();
    int count = getChildCount();
    for (int i = 0; i < count; i++) {
        View child = getChildAt(i);
      // Add a non-top view
        if (!isStickyChild(child)) {
            list.add(child);
        }
    }

    for (int i = 0; i < count; i++) {
        View child = getChildAt(i);
      // Add top view
        if (isStickyChild(child)) {
            list.add(child);
        }
    }
    mViews.clear();
    mViews.addAll(list);
}
Copy the code

DrawingPosition returns the subscript of the view to be drawn, so we need to know the final drawing order in advance so that we can find the corresponding index from the drawingPosition. So you need to sort the view in advance. I chose onLayout for sorting because in my case, child views can be added, removed, and setLayoutParams can change the sorting, and all of these operations happen to re-call the onLayout method of the parent layout. The final sorting method is to add non-top view first, then add top view, so as to ensure that the top view in the last drawing, view overlap will not be covered by other views.

Finally, override getChildDrawingOrder

@Override
protected int getChildDrawingOrder(int childCount, int drawingPosition) {
    if (mViews.size() > drawingPosition) {
      // Find the child view according to drawingPosition and return the index of the child view in the ViewGroup
        return indexOfChild(mViews.get(drawingPosition));
    }
    return super.getChildDrawingOrder(childCount, drawingPosition);
}
Copy the code

At this point, our functionality is complete.

Write in the last

The focus of this article is on the getChildDrawingOrder method, but if I just wanted to show you that there is such a method, there would be no need to write this article. My main purpose in writing this article is to document the process of solving this problem, and there will be pitfalls, as well as serendipities. Some friends on the Internet joke that during the interview, the interviewer will ask: “What problems did you meet and how did you solve them at last?” A lot of people don’t know how to answer that, because all problems that have been solved are not problems, and problems that haven’t been solved you don’t talk about. Take my question. If I had known there was a way, would this still be a problem? We tend to ignore the process of solving the problem, or even the problem itself, and decide that the problem is so simple. But I do not know that this process is the most meaningful and fruitful for us.

Finally, finding the answer from the source code is always the most effective way to solve a problem.