When Can I First Measure a View?
by using the global listener, you may run into an infinite loop.
i fixed this by calling
whateverlayout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
inside of the onGlobalLayout method within the listener.
Concerning the view tree observer:
The returned ViewTreeObserver observer is not guaranteed to remain valid for the lifetime of this View. If the caller of this method keeps a long-lived reference to ViewTreeObserver, it should always check for the return value of isAlive().
Even if its a short lived reference (only used once to measure the view and do the same sort of thing you're doing) I've seen it not be "alive" anymore and throw an exception. In fact, every function on ViewTreeObserver will throw an IllegalStateException if its no longer 'alive', so you always have to check 'isAlive'. I had to do this:
if(viewToMeasure.getViewTreeObserver().isAlive()) {
viewToMeasure.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if(viewToMeasure.getViewTreeObserver().isAlive()) {
// only need to calculate once, so remove listener
viewToMeasure.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
// you can get the view's height and width here
// the listener might not have been 'alive', and so might not have been removed....so you are only
// 99% sure the code you put here will only run once. So, you might also want to put some kind
// of "only run the first time called" guard in here too
}
});
}
This seems pretty brittle to me. Lots of things - albeit rarely - seem to be able to go wrong.
So I started doing this: when you post a message to a View, the messages will only be delivered after the View has been fully initialized (including being measured). If you only want your measuring code to run once, and you don't actually want to observe layout changes for the life of the view (cause it isn't being resized), then I just put my measuring code in a post like this:
viewToMeasure.post(new Runnable() {
@Override
public void run() {
// safe to get height and width here
}
});
I haven't had any problems with the view posting strategy, and I haven't seen any screen flickers or other things you'd anticipate might go wrong (yet...)
I have had crashes in production code because my global layout observer wasn't removed or because the layout observer wasn't 'alive' when I tried to access it
I didn't know about ViewTreeObserver.addOnPreDrawListener()
, and I tried it in a test project.
With your code it would look like this:
public void onCreate() {
setContentView(R.layout.main);
final TextView tv = (TextView)findViewById(R.id.image_test);
final LayerDrawable ld = (LayerDrawable)tv.getBackground();
final ViewTreeObserver obs = tv.getViewTreeObserver();
obs.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw () {
Log.d(TAG, "onPreDraw tv height is " + tv.getHeight()); // bad for performance, remove on production
int height = tv.getHeight();
int topInset = height / 2;
ld.setLayerInset(1, 0, topInset, 0, 0);
tv.setBackgroundDrawable(ld);
return true;
}
});
}
In my test project onPreDraw()
has been called twice, and I think in your case it may cause an infinite loop.
You could try to call the setBackgroundDrawable()
only when the height of the TextView
changes :
private int mLastTvHeight = 0;
public void onCreate() {
setContentView(R.layout.main);
final TextView tv = (TextView)findViewById(R.id.image_test);
final LayerDrawable ld = (LayerDrawable)tv.getBackground();
final ViewTreeObserver obs = mTv.getViewTreeObserver();
obs.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw () {
Log.d(TAG, "onPreDraw tv height is " + tv.getHeight()); // bad for performance, remove on production
int height = tv.getHeight();
if (height != mLastTvHeight) {
mLastTvHeight = height;
int topInset = height / 2;
ld.setLayerInset(1, 0, topInset, 0, 0);
tv.setBackgroundDrawable(ld);
}
return true;
}
});
}
But that sounds a bit complicated for what you are trying to achieve and not really good for performance.
EDIT by kcoppock
Here's what I ended up doing from this code. Gautier's answer got me to this point, so I'd rather accept this answer with modification than answer it myself. I ended up using the ViewTreeObserver's addOnGlobalLayoutListener() method instead, like so (this is in onCreate()):
final TextView tv = (TextView)findViewById(R.id.image_test);
ViewTreeObserver vto = tv.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
LayerDrawable ld = (LayerDrawable)tv.getBackground();
ld.setLayerInset(1, 0, tv.getHeight() / 2, 0, 0);
}
});
Seems to work perfectly; I checked LogCat and didn't see any unusual activity. Hopefully this is it! Thanks!