Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android - reacting after initial layout

Tags:

android

This question has been asked several times in various forms but I haven't found a definitive answer.

I need to be able to get dimensions and positions of descendant Views after the initial layout of a container (a contentView). The dimensions required in this case are of a View(Group) that is not predictable - might have multiple lines of text, minimum height to accommodate decoration, etc. I don't want to do it every on every layout pass (onLayout). The measurement I need is a from a deeply nested child, so overriding the onLayout/onMeasure of each container in between seems a poor choice. I'm not going to do anything that'll cause a loop (trigger-event-during-event).

Romain Guy hinted once that we could just .post a Runnable in the constructor of a View (which AFAIK would be in the UI thread). This seems to work on most devices (on 3 of the 4 I have personally), but fails on Nexus S. Looks like Nexus S doesn't have dimensions for everything until it's done one pass per child, and does not have the correct dimensions when the posted Runnable's run method is invoked. I tried counting layout passes (in onLayout) and comparing to getChildCount, which again works on 3 out of 4, but a different 3 (fails on Droid Razr, which seems to do just one pass - and get all measurements - regardless of the number of children). I tried various permutations of the above (posting normally; posting to a Handler; casting getContext() to an Activity and calling runOnUiThread... same results for all).

I ended up using a horrible shameful no-good hack, by checking the height of the target group against its parent's height - if it's different, it means it's been laid out. Obviously, not the best approach - but the only one that seems to work reliably between various devices and implementations. I know there's no built-in event or callback we can use, but is there a better way?

TYIA

/EDIT The bottom line for me I guess is that the Nexus S does not wait until after layout has completed when .posting() a Runnable, as is the case on all other devices I've tested, and as suggested by Romain Guy and others. I have not found a good workaround. Is there one?

like image 548
momo Avatar asked Dec 16 '22 16:12

momo


2 Answers

I've been successful using the OnGlobalLayoutListener.

It worked great on my Nexus S

final LinearLayout marker = (LinearLayout) findViewById(R.id.distance_marker);
OnGlobalLayoutListener listener = new OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        //some code using marker.getHeight(), etc.
        markersContainer.getViewTreeObserver().removeGlobalOnLayoutListener(this);
    }
};
marker.getViewTreeObserver().addOnGlobalLayoutListener(listener);
like image 163
HannahMitt Avatar answered Jan 07 '23 19:01

HannahMitt


My own experience with OnGlobalLayoutListener is that it does not always wait to fire until all of the child views have been laid out. So, if you're looking for the dimensions of a particular child (or child of a child, etc.) view, then if you test them within the onGlobalLayout() method, you may find that they reflect some intermediate state of the layout process, and not the final (quiescent) state that will be displayed to the user.

In testing this, I noticed that by performing a postDelayed() with a one second delay (obviously, a race condition, but this was just for testing), I did get a correct value (for a child of a child view), but if I did a simple post() with no delay, I got the wrong value.

My solution was to avoid OnGlobalLayoutListener, and instead use a straight override of dispatchDraw() in my top-level view (the one containing the child whose child I needed to measure). The default dispatchDraw() draws all of the child views of the present view, so if you post a call to your code that performs the measurements after calling super.dispatchDraw(canvas) within dispatchDraw(), then you can be sure that all child views will be drawn prior to taking your measurements.

Here is what that looks like:

@Override
protected void dispatchDraw(Canvas canvas) {
  //Draw the child views
  super.dispatchDraw(canvas);

  //All child views should have been drawn now, so we should be able to take measurements
  // of the global layout here
  post(new Runnable() {
    @Override
    public void run() {
      testAndRespondToChildViewMeasurements();
    }
  });
}

Of course, drawing will occur prior to your taking these measurements, and it will be too late to prevent that. But if that is not a problem, then this approach seems to very reliably provide the final (quiescent) measurements of the descendant views.

Once you have the measurements you need, you will want to set a flag at the top of testAndRespondToChildViewMeasurements() so that it will then return without doing anything further (or to make a similar test within the dispatchDraw() override itself), because, unlike an OnGlobalLayoutListener, there is no way to remove this override.

like image 43
Carl Avatar answered Jan 07 '23 20:01

Carl