getChildDrawingOrder called/used erratically?

2019-02-22 08:42发布

问题:

I am creating an isometric map with simple tiles, and I’ve extended RelativeLayout to create a layout that holds these tiles. Really, just using a RelativeLayout as-is worked fine as long as my orientation matched the order in which the tiles were written to the XML file; all I’ve overwritten are the constructors, where I simply call super and setChildrenDrawingOrderEnabled(true); along with setting up some variables (height and width of the grid), and then getChildDrawingOrder itself.

My code for getChildDrawingOrder figures out the new index for the given child, and also sets a string in the child to i->i', where i is the original index and i' is the new index. I’m using this for testing; the children are set to draw this string on themselves along with their coordinates.

Unfortunately, it does not work correctly, or rather it works erratically. Of the nine tiles in my test case, three don’t seem to have getChildDrawingOrder called at all: the string I mentioned above is null. Of the rest, at least one is being drawn out of order despite getting the correct index passed to it.

Here’s a picture (in the TOP orientation):

Notice that (0,2), (1,2), and (2,1) are all listed as NULL, and therefore getChildDrawingOrder appears to have never been called for them. Also note that (1,0) is drawn on top of (1,1), even though both its i (3) and i' (1) are less than (1,1)’s (4 and 4, respectively).

Here’s the code from getChildDrawingOrder:

@Override
protected int getChildDrawingOrder(int childCount, int i)
{
    TileView ch = (TileView)getChildAt(i);
    ch.order = "Called"; // this string is drawn on my children
    int gx, gy; // the "true" x,y for the current rotation,
                // where 0,0 is the top corner
    switch (rotation)
    {
    case TOP:
        gx = ch.x();
        gy = ch.y();
        break;
    case LEFT:
        gx = (width()-1-ch.x());
        gy = ch.y();
        break;
    case RIGHT:
        gx = ch.x();
        gy = (length()-1-ch.y());
        break;
    case BOTTOM:
        gx = (width()-1-ch.x());
        gy = (length()-1-ch.y());
        break;
    default:
        gx = ch.x();
        gy = ch.y();
    }
    int row = gx+gy; // current row
    if ( row == 0 ) // row 0 is always just the top corner and 0
    {
        ch.order = new String(i+"->0"); // string set to i->i'
        return 0;
    }
    else
    {
        int mx = width()-1, // maximum x value
            my = length()-1, // maximum y value
            mrow = mx+my, // maximum row
            min = Math.min(mx, my), // minor axis length
            maj = Math.max(mx, my), // major axis length
            retn; // for storing the return value
        // inside the top corner
        if ( row <= min )
        {
            // Gauss's formula to get number of cells in previous rows
            // plus the number for which cell in this row this is.
            retn = row*(row+1)/2+gy;
        }
        // in the middle
        else if ( row <= maj )
        {
            // Gauss's formula to get number of cells in top corner
            // plus the number of cells in previous rows of the middle section
            // plus the number for which cell in this row this is.
            retn = min*(min+1)/2+min*(row-min)+gy;
        }
        // bottom corner
        else
        {
            retn = (min+1)*(min+2)/2 // cells in the top corner
                 + min*(maj-min) // cells in the middle
                 + (mrow-maj)*(mrow-maj+1)/2 // total cells in bottom triangle
                 - (mrow-row+1)*(mrow-row+2)/2 // less cells after this one
                 + gy // which cell in this row
                 - (row-maj) // to account for gy not starting at zero
                 ;
        }
        ch.order = new String(i+"->"+retn); // string set to i->i'
        return retn;
    }
}

Can anyone shed some light on what’s going on? Why isn’t getChildDrawingOrder being called for those three tiles? Why is (1,0) drawn in the wrong order, even though getChildDrawingOrder is called on it?

回答1:

OK, figured it out by looking at the Android source code. I had the mapping of getChildDrawingOrder: the i passed is “which child should I draw i th?” not "when should I draw child i?" The reason for the NULLs is because those children were being drawn before their own i was passed.

I changed my code to figure out the order for all children during the onMeasure pass, saving that in a SparseIntArray, and then just returned that from getChildDrawingOrder. This works.

Back-calculating the index in the getChildDrawingOrder function, by the way, is a bad idea unless you want to rely on the order in which the children are declared. Because if you don’t rely on that order, you have to walk through the list of children to find the one that has the appropriate x and y values, which means you have to walk through the list of children for each child. That’s an O(n²) operation (read: fairly inefficient). The mathematics are also reasonably complicated.



回答2:

Here is a simple example that shows how to override getChildDrawingOrder

  1. as a way to adjust which of the children gets drawn in which order (with the last on being on top)
  2. have the order change from a tap/click event

The RelativeLayout class:

public class AlternatingChildDrawingOrderRelativeLayout extends RelativeLayout {

    // the childDrawingOrder modifier
    private int childDrawingOrderModifier = 0;

    public AlternatingChildDrawingOrderRelativeLayout(Context context) {
        super(context);
        init(context);
    }

    void init(Context context) {
        setClickable(true);
        setChildrenDrawingOrderEnabled(true);
        setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                // increment to adjust the child drawing order
                childDrawingOrderModifier++;
                // call invalidate to redraw with new order modifier
                ViewCompat.postInvalidateOnAnimation(v);
            }
        });
    }

    @Override
    protected int getChildDrawingOrder(int childCount, int i) {
        // increment i with the modifier, then afford for out of bounds using modulus of the child count
        int returnValue = (i + childDrawingOrderModifier) % childCount;
        Log.v(VIEW_LOG_TAG, "getChildDrawingOrder returnValue=" + returnValue + " i=" + i);
        return returnValue;
    }

    public AlternatingChildDrawingOrderRelativeLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public AlternatingChildDrawingOrderRelativeLayout(Context context,
                                                      AttributeSet attrs,
                                                      int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public AlternatingChildDrawingOrderRelativeLayout(Context context,
                                                      AttributeSet attrs,
                                                      int defStyleAttr,
                                                      int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context);
    }

}

The xml layout

<?xml version="1.0" encoding="utf-8"?>
<com.example.ui.AlternatingChildDrawingOrderRelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="300dp"
    android:layout_height="300dp">

    <View
        android:id="@+id/red"
        android:layout_width="125dp"
        android:layout_height="300dp"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:background="#f00"
        />

    <View
        android:id="@+id/green"
        android:layout_width="125dp"
        android:layout_height="300dp"
        android:layout_centerInParent="true"
        android:background="#0f0"/>

    <View
        android:id="@+id/blue"
        android:layout_width="125dp"
        android:layout_height="300dp"
        android:layout_alignParentRight="true"
        android:layout_alignParentEnd="true"
        android:background="#00f"/>

</com.example.ui.AlternatingChildDrawingOrderRelativeLayout>

What it looks like

Pictured on the left is the starting drawing order, which is the default based on the xml layout:

Red = index 0
Green = index 1
Blue = index 2  

So from first to last (or think bottom to top) that is: Red, Green, Blue. Here is the log dump of getChildDrawingOrder being called

V/View: getChildDrawingOrder returnValue=0 i=0
V/View: getChildDrawingOrder returnValue=1 i=1
V/View: getChildDrawingOrder returnValue=2 i=2

In the middle, after our first tap, the order changes to Green, Blue, Red

V/View: getChildDrawingOrder returnValue=1 i=0
V/View: getChildDrawingOrder returnValue=2 i=1
V/View: getChildDrawingOrder returnValue=0 i=2

And, on the right side shows us what it looks like after our second tap since the order change to: Blue, Red, Green.

V/View: getChildDrawingOrder returnValue=2 i=0
V/View: getChildDrawingOrder returnValue=0 i=1
V/View: getChildDrawingOrder returnValue=1 i=2

Any tap after this pretty much loops it back to the original order where blue was last to be drawn due to the modulus calculation

HTHs!