63

So I have a bit of confusion with trying to set the background drawable of a view as it is displayed. The code relies upon knowing the height of the view, so I can't call it from onCreate() or onResume(), because getHeight() returns 0. onResume() seems to be the closest I can get though. Where should I put code such as the below so that the background changes upon display to the user?

    TextView tv = (TextView)findViewById(R.id.image_test);
    LayerDrawable ld = (LayerDrawable)tv.getBackground();
    int height = tv.getHeight(); //when to call this so as not to get 0?
    int topInset = height / 2;
    ld.setLayerInset(1, 0, topInset, 0, 0);
    tv.setBackgroundDrawable(ld);
luizfzs
  • 1,328
  • 2
  • 18
  • 34
Kevin Coppock
  • 133,643
  • 45
  • 263
  • 274

9 Answers9

71

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!

hata
  • 11,633
  • 6
  • 46
  • 69
Gautier Hayoun
  • 2,922
  • 24
  • 17
  • 1
    Interesting. I do remember reading something about an infinite loop using predraw, while I was doing research on this. I'll look into this suggestion a bit more though, thank you for the code. What is the "mTv" variable you are using for getViewTreeObserver()? – Kevin Coppock Dec 09 '10 at 13:20
  • It's the name of the equivalent of your tv variable in my test project. – Gautier Hayoun Dec 09 '10 at 14:47
  • Gotcha! I'll try that later then. – Kevin Coppock Dec 09 '10 at 16:02
  • Thanks to all. This was painful trying to figure out without your help. +1 . . . – XIVSolutions May 01 '11 at 07:28
  • 2
    +1 This is such a common problem on all platforms, yet the answer is so difficult to find. At least I now have Android wrapped up. – Richard Le Mesurier Feb 17 '12 at 04:56
  • 6
    I would unregister the listener as soon as you do your computation as you ll end up with that code being called constantly – charroch May 14 '12 at 15:51
  • 1
    how can I unregister the listener after execution? – ffleandro Nov 16 '12 at 17:43
  • 11
    @ffleandro You need to save `tv` as a class member. Then the last line of `onGlobalLayout()` should be `tv.getViewTreeObserver().removeGlobalOnLayoutListener(this);` (or `removeOnGlobalLayoutListener` in the v16+ API.) – Dan Is Fiddling By Firelight Mar 06 '13 at 15:49
  • adding GlobalLayout Listener still returns view size 0 after activity is being recreated due to low memory kill by Framework! – Muhammad Babar Dec 01 '14 at 11:59
  • 1
    You really should do @DanNeely's recommendation else it'll result in an infinite loop. Use remove calls for any other ViewTreeObserver at the beginning of the `onGlobalLayout` or `onPreDraw` or whatever – Tim Kist Oct 21 '15 at 11:19
  • `addOnGlobalLayoutListener` this not work in RecyclerView.Adapter check my question any help very appreciate. https://stackoverflow.com/questions/60790803/using-viewtreeobserver-i-am-adding-setmaxlines-and-setellipsize-in-materialtextv – Kishan Viramgama Mar 22 '20 at 17:20
64

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

Splash
  • 1,310
  • 12
  • 20
  • 4
    I also noticed that adding a listener to ViewTreeObserver global layout within a fragment's onCreateView prevented that fragment from ever being collected, even if I removed it again. Using post() fixed this memory leak issue for me. +1 – Dwighte Jul 03 '13 at 23:07
  • In my case, the height value of a child view tested within the onGlobalLayout() method was wrong (I'm guessing because layout was not fully completed), and even when I did a post() of the code that tested the height, the value was wrong, but when I did a postDelayed() of that same code (one second delay) it came back OK. I assume that this is because onGlobalLayout() is invoked whenever any *part* of the view tree changes, and some part *other* than the child view I was testing might have changed *first*. – Carl Jan 20 '14 at 09:41
  • Thanks for View.post() hint! – trashkalmar Sep 01 '14 at 15:54
  • 1
    "when you post a message to a View, the messages will only be delivered after the View has been fully initialized (including being measured)." - is there a reference for this? – stkent Nov 15 '14 at 23:47
  • @Carl, I also bumped with situation where addOnGlobalLayoutListener or post in fragment had invoked before a content is fully drawn. Even onResume and other events haven't helped. So, a postDelayed with one second delay may help. – CoolMind Aug 17 '16 at 16:02
  • I have got a production crash while i was using ` View.post() ` – Mahmoud Mabrok Feb 27 '22 at 16:01
12

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.

kevinl
  • 4,194
  • 6
  • 37
  • 55
4

In my projects I'm using following snippet:

public static void postOnPreDraw(Runnable runnable, View view) {
    view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
        @Override
        public boolean onPreDraw() {
            try {
                runnable.run();
                return true;
            } finally {
                view.getViewTreeObserver().removeOnPreDrawListener(this);
            }
        }
    });
}

So, inside provided Runnable#run you can be sure that View was measured and execute related code.

Dmitry Zaytsev
  • 23,650
  • 14
  • 92
  • 146
  • Why not just use View.post(Runnable)? – A. Steenbergen Jan 30 '15 at 13:20
  • @Doge I explained with some sort of details here http://stackoverflow.com/a/21253298/926907 – Dmitry Zaytsev Jan 30 '15 at 22:04
  • 2
    @Doge basically, by doing `View.post` you're implicitly *assuming* that `View` will be laid out before message you queued with `post` will execute. While this true in absolute most of the cases (actually, I never saw opposite) I prefer to make things explicit. With `postOnPreDraw` you *guarantee* that `View` is laid out and, therefore, size is available – Dmitry Zaytsev Jan 30 '15 at 22:07
3

You can override onMeasure for TextView:

public MyTextView extends TextView {
   ...
   public void onMeasure (int widthMeasureSpec, int heightMeasureSpec) {
      super.onMeasure(widthMeasureSpec,heightMeasureSpec); // Important !!!
      final int width = getMeasuredHeight();
      final int height = getMeasuredHeight();
      // do you background stuff
   }
   ...
}

I am almost sure that you can do it without any line of code also. I think there is a chance to create layer-list.

Damian Kołakowski
  • 2,731
  • 22
  • 25
  • Hmm, I can give this a try, but I'm looking for a way that I can handle multiple layout events for multiple different types of views. I just need to handle layout events as they are happening. I've found ViewTreeObserver.addOnPreDrawListener() which looks like it may be helpful, I've just got to figure out how to implement it. – Kevin Coppock Dec 09 '10 at 05:06
  • that is really strange for me :) can you present your idea in 100% ? – Damian Kołakowski Dec 09 '10 at 05:26
  • Sure! In this particular instance, I'm playing around with a LayerDrawable, that draws a transparent shape above a gradient (see here: http://stackoverflow.com/questions/4381033/multi-gradient-shapes), but the overlay must be exactly HALF the size of the TextView. Since percentages cannot be used in the XML declaration, it must be set through Java. However, I can't set it until I know the height of the TextView, which I can't determine until after layout. – Kevin Coppock Dec 09 '10 at 06:20
  • my post http://stackoverflow.com/questions/12923312/android-view-instantiation-order shows a problem with relying on onMeasure (fundamentally being that onMeasure isn't called until after onCreate exits, which is a problem if you have a custom view containing childern views whos layout's depend on the dimension of the parents, and you need to populate the view with data in onCreate). – samus Oct 17 '12 at 13:34
1

So summarily, 3 ways to handle the size of view.... hmmm, maybe so!

1) As @Gautier Hayoun mentioned, using OnGlobalLayoutListener listener. However, please remember to call at the first code in listener: listViewProduct.getViewTreeObserver().removeOnGlobalLayoutListener(this); to make sure this listener won't call infinitely. And one more thing, sometime this listener won't invoke at all, it because your view has been drawn somewhere you don't know. So, to be safe, let's register this listener right after you declare a view in onCreate() method (or onViewCreated() in fragment)

view = ([someView]) findViewById(R.id.[viewId]);

// Get width of view before it's drawn.
view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
      @Override
      public void onGlobalLayout() {

        // Make this listener call once.
        view.getViewTreeObserver().removeOnGlobalLayoutListener(this);

        int width = listViewProduct.getWidth();
      });

2) If you want to do something right after the view has been drawn and shown up, just use post() method (of course you can get the size here since your view was drawn completely already). For example,

listViewProduct.post(new Runnable() {
  @Override
  public void run() {
    // Do your stuff here.
  }
});

Moreover, you can use postDelay() with the same purpose, adding a little delay time. Let's try it yourself. You will need it sometime. For prompt example, when you want to scroll the scroll view automatically after adding more items into scroll view or expand or make some more view visible from gone status in scroll view with animation (it means this process will take a little time to show up everything). It will definitely change the scroll view's size, so after the scroll view is drawn, we also need to wait a little time to get an exact size after it adds more view into it. This's a high time for postDelay() method to come to the rescue!

3) And if you want to set up your own view, you can create a class and extend from any View yo like which all have onMeasure() method to define size of the view.

Nguyen Tan Dat
  • 3,780
  • 1
  • 23
  • 24
1
textView.doOnPreDraw {
   //call textView.height
}

You can also use this view extension:

androidx.core.view (ViewKt.class)

public inline fun View.doOnPreDraw(
    crossinline action: (View) → Unit
): OneShotPreDrawListener

Performs the given action when the view tree is about to be drawn. The action will only be invoked once prior to the next draw and then removed.

Ultimo_m
  • 4,724
  • 4
  • 38
  • 60
0

You can get all of view measures in method of class main activity below onCreate() method:

@Override
protected void onCreate(Bundle savedInstanceState){}
@Override
public void onWindowFocusChanged(boolean hasFocus){
    int w = yourTextView.getWidth();
}

So you can redrawing, sorting... your views. :)

nobjta_9x_tq
  • 1,205
  • 14
  • 16
0

Use OnPreDrawListener() instead of addOnGlobalLayoutListener(), because it is called earlier.

  tv.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener()
    {
        @Override
        public boolean onPreDraw()
        {
            tv.getViewTreeObserver().removeOnPreDrawListener(this);
            // put your measurement code here
            return false;
        }
    });
Ayaz Alifov
  • 8,334
  • 4
  • 61
  • 56