Search code examples
androidanimator

Gone'd view draws on top during LayoutTransition.DISAPPEARING animator


I'm using LayoutTransition to fade a view with a translucent background in and out when I set visibility to VISIBLE and GONE respectively. Standard stuff. I have a view with a solid background on top of (after, in XML) that transitioning view. I expect that the user will see that top view with the solid background unchanged throughout the transition, exactly the opposite of the animation that runs when the overlay view appears.

The APPEARING animator works as expected: the user can see the top view throughout the animation. The DISAPPEARING animator does not work as expected: the overlay view ends up drawing on top of all other views.

It's may be worth noting that this happens even if you don't set your own LayoutTransition and instead rely on android:animateLayoutChanges="true" in the XML; I added my own to increase the duration, making it easier to see the transition.

Any thoughts on how to workaround this issue? I'm guessing it is pretty common and that I must be missing something obvious as this is the default behavior. I've tried a few things like attaching an AnimatorUpdateListener to invalidate the top view every frame, setting my own DISAPPEARING ObjectAnimator with an update listener that invalidates the top view every frame, and replacing the overlay view with a TextView and other view types just in case FrameLayout behaves in some special way.

If I replace the transition animators with a regular ObjectAnimator I get the expected behavior, except that the view is not GONE and thus accepts touch events and all that junk (which makes that "solution" untenable). Thus I don't think that the issue is merely that the transitioning view has an associated animator. It seems that it is specifically an issue with the LayoutTransition code or something that calls said.

MainActivity:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final View overlay = findViewById(R.id.overlay);

        final LayoutTransition lt = new LayoutTransition();

        lt.setDuration(LayoutTransition.APPEARING, 300);
        lt.setStartDelay(LayoutTransition.APPEARING, 0);
        lt.setDuration(LayoutTransition.DISAPPEARING, 1000);
        lt.setStartDelay(LayoutTransition.DISAPPEARING, 0);
        ((ViewGroup) overlay.getParent()).setLayoutTransition(lt);

        final Runnable runnable = new Runnable() {
            @Override
            public void run() {
                if (overlay.getVisibility() == View.VISIBLE) {
                    overlay.setVisibility(View.GONE);
                } else {
                    overlay.setVisibility(View.VISIBLE);
                }

                overlay.postDelayed(this, 1500);
            }
        };

        overlay.post(runnable);
    }
}

activity_main.xml:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="#ffffff"
                android:paddingBottom="@dimen/activity_vertical_margin"
                android:paddingLeft="@dimen/activity_horizontal_margin"
                android:paddingRight="@dimen/activity_horizontal_margin"
                android:paddingTop="@dimen/activity_vertical_margin"
                tools:context=".MainActivity"
    >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#ff0000"
        android:text="THIS IS BEHIND THE OVERLAY AND THUS SHOULD TINT"
        android:textColor="#ffffff"
        />

    <FrameLayout
        android:id="@+id/overlay"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#7f00ff00"
        />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_centerInParent="true"
        android:layout_margin="64dp"
        android:background="#ffffff"
        android:gravity="center"
        android:text="THIS VIEW IS IN FRONT OF THE OVERLAY AND THUS SHOULD NOT SUFFER TINTING"
        android:textColor="#000000"
        android:textSize="32sp"
        />

</RelativeLayout>

My device runs API 22 and I've set targetSdkVersion to 22 as well. Basically I created a whole new project and modified the generated MainActivity and activity_main.xml to match these pasted files almost exactly (I've only excluded the import and package lines for brevity).


Solution

  • I came cross the same problem today, so I checked out the ViewGroup.java source code. The result is that disappearing children always draw on the others.

    This is a snippet of ViewGroup.dispatchDraw(Canvas) in API 23 and I am pretty sure it is almost the same in API 22.

    for (int i = 0; i < childrenCount; i++) {
        while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
            final View transientChild = mTransientViews.get(transientIndex);
            if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                    transientChild.getAnimation() != null) {
                more |= drawChild(canvas, transientChild, drawingTime);
            }
            transientIndex++;
            if (transientIndex >= transientCount) {
                transientIndex = -1;
            }
        }
        int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
        final View child = (preorderedList == null)
                ? children[childIndex] : preorderedList.get(childIndex);
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
            more |= drawChild(canvas, child, drawingTime);
        }
    }
    while (transientIndex >= 0) {
        // there may be additional transient views after the normal views
        final View transientChild = mTransientViews.get(transientIndex);
        if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                transientChild.getAnimation() != null) {
            more |= drawChild(canvas, transientChild, drawingTime);
        }
        transientIndex++;
        if (transientIndex >= transientCount) {
            break;
        }
    }
    if (preorderedList != null) preorderedList.clear();
    
    // Draw any disappearing views that have animations
    if (mDisappearingChildren != null) {
        final ArrayList<View> disappearingChildren = mDisappearingChildren;
        final int disappearingCount = disappearingChildren.size() - 1;
        // Go backwards -- we may delete as animations finish
        for (int i = disappearingCount; i >= 0; i--) {
            final View child = disappearingChildren.get(i);
            more |= drawChild(canvas, child, drawingTime);
        }
    }
    

    Gone'd views are in mDisappearingChildren.

    As the source code said, normal views and transient views draw first, then disappearing views draw. So disappearing children always draw on the others. App developers can not change the order.

    My suggestion is don't use LayoutTransition, write animation by yourself.

    Edit:

    I found a trick to draw disappearing children before other views, reflect required.

    You need a dump view to make disappearing children replace it in ViewGroup.drawChild(Canvas, View, long).

    An example is here.

    public class TrickLayout extends FrameLayout {
    
        private Field mDisappearingChildrenField;
        private ArrayList<View> mSuperDisappearingChildren;
        // The dump view to draw disappearing children
        // Maybe you need more than one dump view
        private View mDumpView;
        private boolean mDoTrick;
    
        public TrickLayout(Context context) {
            super(context);
            init(context);
        }
    
        public TrickLayout(Context context, AttributeSet attrs) {
            super(context, attrs);
            init(context);
        }
    
        public TrickLayout(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            init(context);
        }
    
        private void init(Context context) {
            try {
                mDisappearingChildrenField = ViewGroup.class.getDeclaredField("mDisappearingChildren");
                mDisappearingChildrenField.setAccessible(true);
            } catch (NoSuchFieldException e) {
                // Ignore
            }
    
            if (mDisappearingChildrenField != null) {
                // You can add dump view in xml or somewhere else in code
                mDumpView = new View(context);
                addView(mDumpView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
            }
        }
    
    
        @SuppressWarnings("unchecked")
        private void getSuperDisappearingChildren() {
            if (mDisappearingChildrenField == null || mSuperDisappearingChildren != null) {
                return;
            }
    
            try {
                mSuperDisappearingChildren = (ArrayList<View>) mDisappearingChildrenField.get(this);
            } catch (IllegalAccessException e) {
                // Ignore
            }
        }
    
        private boolean iWantToDoTheTrick() {
            // Do I need do the trick?
            return true;
        }
    
        private boolean beforeDispatchDraw() {
            getSuperDisappearingChildren();
    
            if (mSuperDisappearingChildren == null ||
                    mSuperDisappearingChildren.size() <= 0 || getChildCount() <= 1) { // dump view included
                return false;
            }
    
            return iWantToDoTheTrick();
        }
    
        private void afterDispatchDraw() {
            // Clean up here
        }
    
        @Override
        protected void dispatchDraw(Canvas canvas) {
            mDoTrick = beforeDispatchDraw();
            super.dispatchDraw(canvas);
            if (mDoTrick) {
                afterDispatchDraw();
                mDoTrick = false;
            }
        }
    
        @Override
        protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
            ArrayList<View> disappearingChildren = mSuperDisappearingChildren;
    
            if (mDoTrick) {
                if (child == mDumpView) {
                    boolean more = false;
                    for (int i = disappearingChildren.size() - 1; i >= 0; i--) {
                        more |= super.drawChild(canvas, disappearingChildren.get(i), drawingTime);
                    }
                    return more;
                } else if (disappearingChildren.contains(child)) {
                    // Skip
                    return false;
                }
            }
    
            return super.drawChild(canvas, child, drawingTime);
        }
    }