Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Changing LayoutParams on an Android view has no effect, even after calling requestLayout

I've run into this situation two or three times, where I have some code that looks like this:

  ViewGroup.LayoutParams params = myView.getLayoutParams();
  params.height = 240;
  myView.requestLayout();

Or alternatively:

  ViewGroup.LayoutParams params = myView.getLayoutParams();
  params.height = 240;
  myView.setLayoutParams(params);

And the view size never changes. I've tried the following, with no luck:

  1. Calling forceLayout, which ironically seems less forceful than requestLayout, because it doesn't notify the View's parents that a layout is needed.
  2. Overriding onMeasure() to see if it is ever called (it isn't).
  3. Making sure that I'm not making the call to requestLayout during a Layout pass.

I've also tried calling requestLayout on all of the View's parents, like so:

  ViewParent parent = myView.getParent();
  while (parent != null) {
      parent.requestLayout();
      parent = parent.getParent();
  }

This works, but it seems really hacky. I'd much rather find the real solution.

What am I doing wrong?

like image 499
fpsulli3 Avatar asked Jul 02 '17 16:07

fpsulli3


1 Answers

If you are observing this behavior, it's likely that some view in your hierarchy has requested layout on a background thread, which is a no-no.

In order to find the offending view, step into your call to requestLayout and look for a variable called mAttachInfo.mViewRequestingLayout. That reference will likely point to the view that made the bad call. You can then search your code for any places where requestLayout or setLayoutParams is being called on this view.

If you find and fix any cases where these calls are being made from a background thread, this will likely fix the problem.

Background / Explanation

Suppose you have a view hierarchy that looks something like this:

view hierarchy

Then, suppose that NaughtyView requests a layout on a background thread.

In order for NaughtyView to actually get measured during the next layout pass, it needs to notify its parents, recursively, until the view root is notified:

view root ignores request

Note that each view sets a flag on itself, indicating that it has requested layout. Also, note that the view root has ignored the request. But why?

There is a specific situation where the view root may ignore the layout request, and it's a bit subtle. To see that situation, let's look at the implementation of ViewRootImpl.requestLayout:

@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

A brief aside on how layout requests are handled. You're not supposed to call requestLayout while a layout pass is in progress. If you do so anyway, the view root will try to accommodate your request by laying out your view in a second pass. If the mHandlingLayoutInLayoutRequest member variable is true, it means that the view root is currently running that second pass. During that phase, the view root ignores incoming layout requests, and so mLayoutRequested will not be set to true.

Assuming all calls to requestLayout are happening on the ui thread like they're supposed to, this behavior is harmless (in fact, desired).

However, if you make a call to requestLayout on a background thread, it can mess up the sequence of events and leave your view hierarchy in the state resembling the diagram above, with a string of ancestors who have their "layout requested" flags set, but a view root who is not aware of it.

So next, suppose that someone calls requestLayout on NiceView. This time, the call is made properly on the ui thread:

layout request blocked

As usual, NiceView tries to notify all of its ancestors. However, once this notification reaches NiceView's common ancestor with NaughtyView, the traversal is blocked. This happens because each view only notifies its parent if its parent hasn't already been notified (as indicated by the aforementioned flag).

Since the traversal never reaches the view root, the layout never happens.

This issue can spontaneously fix itself if some of other view in the hierarchy — any view that does not share an ancestor with NaughtyView other than the view root — happens to request a layout (properly, on the ui thread). When this happens, the view root's own "layout requested" flag will be set to true, and it will finally perform a layout on all of the views that have requested it (including NaughtyView and NiceView). However, for some user interfaces, you can't count on this happening before the user notices.

like image 129
fpsulli3 Avatar answered Nov 19 '22 18:11

fpsulli3