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).
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);
}
}