ViewGroup draws subviews in order by default. What scenes need to change the drawing order?

Today we’re going to talk about a small detail of the View drawing process, the custom drawing order.

View of the three processes: measurement, layout, drawing, I think we should be familiar with the heart. In the drawing phase, the ViewGroup not only needs to draw itself, but also needs to draw its children. This drawing strategy defaults to sequential drawing, i.e. [0 ~ childCount).

Is there any way to adjust the default policy? For example, change it to (childCount ~ 0), or change a View to draw last. At the same time, what scenes did we need to make this change?

It is important to note that the drawing order affects the overlay order, and also affects the event distribution of the View. These are related effects, which can be described as affecting the whole.

Let’s talk about that today.

2. Item processing of TV App

Modify the View drawing order, in daily development, basic use. The UI design of many mobile apps is mostly flat, except for some very special custom views. In most cases, we do not need to consider the default drawing order of the View.

This makes sense, because normally, a View added later in a ViewGroup should be visually overlaid on top of the previous View.

But there is one scene that is very special, and that is the Android TV App.

In the design of TV, it is necessary to deal with the change of the focus state of View in order to enrich the visual experience because the remote control button is needed to control.

For example, it’s common to get a focus ItemView and highlight it, zoom in and add a shadow.

Then this brings a problem. Normally, we use RecyclerView to realize the list effect. When the distance between items is too small, a single Item will be enlarged and the effect of covering will appear.

As you can see in the image above, a very common focus magnification and highlighting is obscured by the View behind it.

How to solve such a situation?

 

Pat head thinking, since is the spacing is too small, that we widen the spacing is good. Modify a property to solve a requirement and the designer is crying at the workstation.

However, there are some design effects that are sufficiently spaced so that there is no hiding, such as some pages on the Bilibili TV terminal.

But we can’t solve the problem by just changing the spacing, and in most cases, designers leave us with very little spacing. Most TV apps do that.

If we can’t get away with it, we need to figure out how to do it.

3. Modify the principle of drawing sequence

Changing the drawing order is actually quite simple, Android has already set aside extension points for us.

We know that a ViewGroup stores subviews through its members, the mChildren array. In the dispatchDraw() method loop where the ViewGroup draws child views, the values are not directly indexed from the mChildren array.

@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); / / is not directly obtained from the mChildren final View child = getAndVerifyPreorderedView (preorderedList, children, childIndex); if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() ! = null) { more |= drawChild(canvas, child, drawingTime); }} / /... }Copy the code

As you can see, the child is not from mChildren directly take, but by getAndVerifyPreorderedView (), its parameters in addition to the children, there will be a preorderedList ArrayList, And the index of the child 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

In this case, if preorderedList is not empty, the child View is fetched from it, otherwise it is fetched from children.

Going back to dispatchDraw(), the preorderedList key list used here, from buildOrderedChildList(), In a method through getAndVerifyPreorderedIndex () to obtain corresponding index of View, this method requires a Boolean type customOrder, indicates that whether the custom order is needed.

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;
}
Copy the code

BuildOrderedChildList () has the logic to adjust the children order along the Z-axis. If the z-axis values are the same, refer to the customOrder configuration.

Usually the sub-views in a ViewGroup have the same Z value, so the key parameter is the customOrder switch.

Learned from the code customOrder by isChildrenDrawingOrderEnabled () method to obtain, With the matching is setChildrenDrawingOrderEnabled () can set customOrder values.

In other words, if we want to adjust the order, only 2 steps are needed:

  1. callsetChildrenDrawingOrderEnable(true)Enable custom drawing sequence
  2. rewritegetChildDrawingOrder()Modify the value index of a View

Four, the instance,

Finally, we will write a Demo to rewrite the RecycleView getChildDrawingOrder() method to achieve the focus of the 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);
}
Copy the code

Don’t forget to still need to call setChildrenDrawingOrderEnabled open custom drawing order (true).

At this point, when the focus is enlarged, it will not be blocked by other views.

From: mp.weixin.qq.com/s/G3BKLbu1g…


Extension:

If you want to specify the drawing order of a ViewGroup and its subclasses, there are only two steps:

1. SetChildrenDrawingOrderEnabled son (true) open the custom View drawing order;

2. Use setZ(float) to customize the Z value, the larger the value, the higher the priority to draw;


Overwrite getChildDrawingOrder to draw the Item in reverse order

To achieve a recent effect, the GridView animates each item and finds that each item is blocked by the following item. This method overwrites the viewGroup to draw the following item in reverse order to draw the following item at the bottom of the previous item.

public class MyGridView extends GridView {
public MyGridView(Context context, AttributeSet attrs) {// constructor
        super(context, attrs);
        setChildrenDrawingOrderEnabled(true);
    }
 
    @Override
    protected int getChildDrawingOrder(int childCount, int i) {
        return childCount - i - 1;/ / reverse}}Copy the code

GridView solves the problem that an item is blocked by other items when zooming in on TV, which cannot be solved using bringToFront alone

We have done the problem that when we use GridView on TV to enlarge an item, it will be blocked by the back or other items. How do we solve this problem?

In fact, when we encounter such a situation, using bringToFront will not solve the problem.

So what we’re going to do is, we’re going to change the order in which the GridView draws its child views, so we’re going to change the order in which the selected item is drawn on the top;

/ * * * *@authorZhanghuagang 2017.7.6 * * /
public class CommonGridView extends GridView {
	private View mLastView = null;
	private int mSelectedPosition;
	/ * * * *@authorZhanghuagang 2017.7.6 * * /
	public CommonGridView(Context context) {
		this(context, null);
	}
 
	public CommonGridView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		setChildrenDrawingOrderEnabled(true);
		setSmoothScrollbarEnabled(true);
	}
 
	public CommonGridView(Context context, AttributeSet attrs) {
		this(context, attrs, 0);
	}
 
	@Override
	protected void setChildrenDrawingOrderEnabled(boolean enabled) {
		super.setChildrenDrawingOrderEnabled(enabled);
	}
 
	public int getSelectedPosition(a) {
		return mSelectedPosition;
	}
 
	public void setSelectedPosition(int mSelectedPosition) {
		this.mSelectedPosition = mSelectedPosition;
	}
	
	@Override
	public void draw(Canvas canvas) {
		super.draw(canvas);
	}
 
	private void zoomInView(View v){
		AnimatorSet animSet = new AnimatorSet();
		float[] values = new float[] { 1.0 f  ,1.18 f  };
		animSet.playTogether(ObjectAnimator.ofFloat(v, "scaleX", values),
				ObjectAnimator.ofFloat(v, "scaleY", values));
		animSet.setDuration(100).start();
	}
	
	private void zoomOutView(View v){
		AnimatorSet animSet = new AnimatorSet();
		float[] values = new float[] { 1.18 f  ,1.0 f  };
		animSet.playTogether(ObjectAnimator.ofFloat(v, "scaleX", values),
				ObjectAnimator.ofFloat(v, "scaleY", values));
		animSet.setDuration(100).start();
	}
	
 
	
	public void onItemSelected(AdapterView<? > parent, View view,int position, long id) {
		if(view! =null)
	    zoomInView(view);
	    if(view ! = mLastView && mLastView! =null) {
	        zoomOutView(mLastView);
	    }
	    mLastView=view;
	}
	
	
 
	
	/** * this method is used to perfect the */ that feels the order of item enlargement is wrong
	@Override
	protected int getChildDrawingOrder(int childCount, int i) {
	    if (this.getSelectedItemPosition() ! = -1) {
	        if (i + this.getFirstVisiblePosition() == this.getSelectedItemPosition()) {// This is the last item that was supposed to be refreshed
	            return childCount - 1;
	        }
	        if (i == childCount - 1) {// This is the last item to refresh
	            return this.getSelectedItemPosition() - this.getFirstVisiblePosition(); }}returni; }}Copy the code

First, we are customizing the view. In the constructor, we set whether we can change the drawing order to True and change it to yes. setChildrenDrawingOrderEnabled(true); Then, cover the key methods. The getChildDrawingOrder method implements the logic of changing the drawing order in this method. If we want to zoom in without being blocked by other items, we must place the drawing order last when it is selected. This method implements the logic as follows.

    /** * this method is used to perfectly solve the problem of item enlargement and drawing order */
    @Override
    protected int getChildDrawingOrder(int childCount, int i) {
        if (this.getSelectedItemPosition() ! = -1) {
            if (i + this.getFirstVisiblePosition() == this.getSelectedItemPosition()) {// This is the last item that was supposed to be refreshed
                return childCount - 1;
            }
            if (i == childCount - 1) {// This is the last item to refresh
                return this.getSelectedItemPosition() - this.getFirstVisiblePosition(); }}return i;
    }
Copy the code

The first line of code checks whether the position of the currently selected item is valid. If the currently selected child view is visible, set it to the last child view to draw. If the last view is selected, return its valid position.