227

I have a custom view that draws a scrollable bitmap to the screen. In order to initialize it, i need to pass in the size in pixels of the parent layout object. But during the onCreate and onResume functions, the Layout has not been drawn yet, and so layout.getMeasuredHeight() returns 0.

As a workaround, i have added a handler to wait one second and then measure. This works, but its sloppy, and I have no idea how much i can trim the time before I end up before the layout gets drawn.

What I want to know is, how can I detect when a layout gets drawn? Is there an event or callback?

RajaReddy PolamReddy
  • 22,428
  • 19
  • 115
  • 166
Plastic Sturgeon
  • 12,527
  • 4
  • 33
  • 47

12 Answers12

421

You can add a tree observer to the layout. This should return the correct width and height. onCreate() is called before the layout of the child views are done. So the width and height is not calculated yet. To get the height and width, put this on the onCreate() method:

    final LinearLayout layout = (LinearLayout) findViewById(R.id.YOUR_VIEW_ID);
    ViewTreeObserver vto = layout.getViewTreeObserver(); 
    vto.addOnGlobalLayoutListener (new OnGlobalLayoutListener() { 
        @Override 
        public void onGlobalLayout() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                    layout.getViewTreeObserver()
                            .removeOnGlobalLayoutListener(this);
                } else {
                    layout.getViewTreeObserver()
                            .removeGlobalOnLayoutListener(this);
                }
            int width  = layout.getMeasuredWidth();
            int height = layout.getMeasuredHeight(); 

        } 
    });
Lakhwinder Singh
  • 6,799
  • 4
  • 25
  • 42
blessanm86
  • 31,439
  • 14
  • 68
  • 79
  • 34
    Thank you very much! But I wonder why there is no simpler method for doing this because it's a common problem. – felixd Mar 16 '14 at 00:03
  • I'm using `PopupWindow.showAsDropdown` in `onGlobalLayoutListener`, but it is shown above the anchor view. The reason is likely to be that at that time `View.getLocatioinOnScreen`return a temporary(not final) value. How can I deal with it? – suitianshi Aug 19 '14 at 06:47
  • It solved the problem. But when is this callback exactly called? What other use-cases can it serve? – Diffy Oct 07 '14 at 04:52
  • 1
    @everyone im happy this solution has worked for many. But if you have any queries please make it another question or it may be already answered. I haven't been doing android native development for almost 2 years so my knowledge is outdated and maybe obsolete. – blessanm86 Oct 07 '14 at 13:32
  • 9
    removeGlobalOnLayoutListener is deprecated in API level 16.Use removeOnGlobalLayoutListener instead. – tounaobun May 20 '15 at 03:24
  • 3
    One "gotcha" I am seeing is if your view is not visible, the onGlobalLayout is called, but later when visible the getView is called, but not the onGlobalLayout...ugh. – Stephen McCormick Dec 09 '15 at 19:53
  • BEWARE: `onGlobalLayout()` is not called when we return from a phone call. Solution: Check `layout.isShown()` first prior to calling `addOnGlobalLayoutListener`. If `isShown()` is true, the view is already ready. If false, we set the listener to know when it is ready. – rpattabi Jun 09 '17 at 08:06
  • 2
    Another quick solution for this is to use `view.post(Runnable)`, it cleaner and it does the same thing. – dvdciri Jun 30 '17 at 14:50
  • It's really hard to get. onStart and onResume are the callbacks where you start to see your activity which means your activity should already have done with the phases in the rendering pipeline (measure, layout, drawing = display list creation + execution) including the layout. But we see that layout has not been completed at onResume. Any explanation would be much appreciated. – stdout Jul 01 '17 at 13:52
  • is there much performance concern if we have a lot of views that we're adding these listeners to? – jacoballenwood Nov 18 '19 at 21:01
  • how to use RecyclerView.Adapter using RecyclerView please check my question https://stackoverflow.com/questions/60790803/using-viewtreeobserver-i-am-adding-setmaxlines-and-setellipsize-in-materialtextv @blessenm – Kishan Viramgama Mar 24 '20 at 11:08
  • Is there a reason why you used `layout.getViewTreeObserver()` instead of the variable `vto` inside the `onGlobalLayout`? – Simone May 11 '20 at 12:26
  • @Simone i think i just missed the variable. vto should be fine. – blessanm86 May 12 '20 at 06:36
  • @blessenm I tried with the variable and I got the exception "This ViewTreeObserver is not alive". I guess it's better to not use the variable, here there is a question related: https://stackoverflow.com/questions/26192686/remove-listener-from-viewtreeobserver – Simone May 12 '20 at 07:07
89

A really easy way is to simply call post() on your layout. This will run your code the next step, after your view has already been created.

YOUR_LAYOUT.post( new Runnable() {
    @Override
    public void run() {
        int width  = YOUR_LAYOUT.getMeasuredWidth();
        int height = YOUR_LAYOUT.getMeasuredHeight(); 
    }
});
Elliptica
  • 3,928
  • 3
  • 37
  • 68
  • 8
    I don't know if post will actually run your code after the view has already been created. There is no guarantee, since post will just add the Runnable that you create to the RunQueue, to be executed after previous queue executed on UI Thread. – HendraWD Jun 09 '16 at 09:14
  • 7
    On the contrary, post() executes after the next UI step update, which happens after the view is created. Therefore, you are guaranteed that it will execute after the view is made. – Elliptica Jun 11 '16 at 01:05
  • I tested this code several times, it works best only in UI-thread. In a background thread it sometimes doesn't work. – CoolMind Aug 04 '16 at 11:00
  • 3
    @HendraWijayaDjiono, maybe this post will answer: http://stackoverflow.com/questions/21937224/does-posting-runnable-to-an-ui-thread-guarantee-that-layout-is-finished-when-it – CoolMind Aug 04 '16 at 11:04
  • Works like a charm. Used on the constructors of my custom view. – htafoya Apr 09 '17 at 18:56
  • 1
    I ran into an issue where using the first answer wasn't working every time, using post on the view I needed to scroll (a scroll view), allowed me to scroll to the desired position correctly as soon as the view was ready to take scrollTo method calls. – Danuofr Sep 12 '18 at 20:08
  • I like this code. But in my case doesn't work. The code inside the run is execute before I can see the layout on the screen. Someone had the same problem? – Boris Karloff Sep 13 '18 at 16:30
  • @BorisKarloff The view is created and initialized before it is drawn. This code guarantees that the view will have been initialized. It may not have been drawn yet. Do you need to "see" the layout on the screen first? If so, you might create a listener in its draw method – Elliptica Sep 14 '18 at 16:36
  • @Elliptica yes i need that. I tried something but it doesn't work. – Boris Karloff Sep 16 '18 at 12:51
  • @BorisKarloff Since your question is actually a variant, why don't you post a question for it with code etc. and put a link here? Then you can get the help you need for your particular case – Elliptica Sep 17 '18 at 23:48
26

To avoid deprecated code and warnings you can use:

view.getViewTreeObserver().addOnGlobalLayoutListener(
        new ViewTreeObserver.OnGlobalLayoutListener() {
            @SuppressWarnings("deprecation")
            @Override
            public void onGlobalLayout() {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                    view.getViewTreeObserver()
                            .removeOnGlobalLayoutListener(this);
                } else {
                    view.getViewTreeObserver()
                            .removeGlobalOnLayoutListener(this);
                }
                yourFunctionHere();
            }
        });
Jon
  • 9,156
  • 9
  • 56
  • 73
murena
  • 1,116
  • 10
  • 4
13

androidx.core-ktx already has this

/**
 * Performs the given action when this view is next laid out.
 *
 * The action will only be invoked once on the next layout and then removed.
 *
 * @see doOnLayout
 */
public inline fun View.doOnNextLayout(crossinline action: (view: View) -> Unit) {
    addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
        override fun onLayoutChange(
            view: View,
            left: Int,
            top: Int,
            right: Int,
            bottom: Int,
            oldLeft: Int,
            oldTop: Int,
            oldRight: Int,
            oldBottom: Int
        ) {
            view.removeOnLayoutChangeListener(this)
            action(view)
        }
    })
}

/**
 * Performs the given action when this view is laid out. If the view has been laid out and it
 * has not requested a layout, the action will be performed straight away, otherwise the
 * action will be performed after the view is next laid out.
 *
 * The action will only be invoked once on the next layout and then removed.
 *
 * @see doOnNextLayout
 */
public inline fun View.doOnLayout(crossinline action: (view: View) -> Unit) {
    if (ViewCompat.isLaidOut(this) && !isLayoutRequested) {
        action(this)
    } else {
        doOnNextLayout {
            action(it)
        }
    }
}
12

Better with kotlin extension functions

inline fun View.waitForLayout(crossinline yourAction: () -> Unit) {
    val vto = viewTreeObserver
    vto.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
        override fun onGlobalLayout() {
            when {
                vto.isAlive -> {
                    vto.removeOnGlobalLayoutListener(this)
                    yourAction()
                }
                else -> viewTreeObserver.removeOnGlobalLayoutListener(this)
            }
        }
    })
}
Sergio
  • 2,346
  • 2
  • 24
  • 28
  • Very nice and elegant solution that can be reused. – Ionut Negru May 08 '20 at 07:47
  • Thanks! Agree, if we call `vto.removeOnGlobalLayoutListener(this)` in `else` branch, we will get `java.lang.IllegalStateException: This ViewTreeObserver is not alive, call getViewTreeObserver() again`. Also this method didn't help in my case in BottomSheetDialog, but `Handler().post` helped. – CoolMind Oct 19 '21 at 14:22
4

An alternative to the usual methods is to hook into the drawing of the view.

OnPreDrawListener is called many times when displaying a view, so there is no specific iteration where your view has valid measured width or height. This requires that you continually verify (view.getMeasuredWidth() <= 0) or set a limit to the number of times you check for a measuredWidth greater than zero.

There is also a chance that the view will never be drawn, which may indicate other problems with your code.

final View view = [ACQUIRE REFERENCE]; // Must be declared final for inner class
ViewTreeObserver viewTreeObserver = view.getViewTreeObserver();
viewTreeObserver.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
    @Override
    public boolean onPreDraw() {
        if (view.getMeasuredWidth() > 0) {     
            view.getViewTreeObserver().removeOnPreDrawListener(this);
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
            //Do something with width and height here!
        }
        return true; // Continue with the draw pass, as not to stop it
    }
});
Abandoned Cart
  • 4,512
  • 1
  • 34
  • 41
jwehrle
  • 4,414
  • 1
  • 17
  • 13
  • The question is `How can you tell when a layout has been drawn?` and you are providing a method where you call an `OnPreDrawListener` and `don't stop interrogating that view until it has provided you with a reasonable answer` so the objections to your solution should be obvious. – Abandoned Cart May 14 '19 at 14:05
  • 1
    What you need to know is not when it's been drawn but when it's been measured. My code answers the question that really needs to be asked. – jwehrle May 18 '19 at 02:37
  • Is there some problem with this solution? Does it fail to solve the problem confronted? – jwehrle May 19 '19 at 00:16
  • 1
    The view is not measured until it is laid out, so this alternative is excessive and performs a redundant check. You admit not addressing the question, but what you feel should be asked. The original comment made both of these points, but there are also some issues with the code itself that have been submitted as an edit. – Abandoned Cart May 20 '19 at 13:18
  • That's a nice edit. Thank you. I guess what I'm asking is, Do you think that this answer should be deleted or not? I encountered the OP's problem. This solved the problem. I offered it in good faith. Was that a mistake? Is that something that should not be done on StackOverflow? – jwehrle May 21 '19 at 18:39
  • That is not for me to decide. – Abandoned Cart May 21 '19 at 19:24
  • If I had the OP's problem, I would be glad to see this answer. That's why I submitted it. – jwehrle May 21 '19 at 20:01
  • Is this really necessary? I can downvote the answer if you prefer. I would have thought fixing a major flaw and correcting the misleading preface was enough. – Abandoned Cart May 21 '19 at 20:22
  • using RecyclerView.Adapter touch screen every time call and hang phone please check my question @jwehrle https://stackoverflow.com/questions/60790803/using-viewtreeobserver-i-am-adding-setmaxlines-and-setellipsize-in-materialtextv – Kishan Viramgama Mar 24 '20 at 11:13
4

The most elegant way to do this is using OneShotPreDrawListener. An OneShotPreDrawListener will register a callback to be invoked when the view tree is about to be drawn and it will remove itself after one OnPreDraw call.

OneShotPreDrawListener.add(view) {
    view.doSomething();
}
UdaraWanasinghe
  • 2,622
  • 2
  • 21
  • 27
3

Another answer is:
Try checking the View dimensions at onWindowFocusChanged.

Dr.jacky
  • 3,341
  • 6
  • 53
  • 91
Joe Plante
  • 6,308
  • 2
  • 30
  • 23
2

I recommend you to use doOnLayout if you're using kotlin

doOnLayout {
    doWhateverYouNeed()
}

doc: Performs the given action when this view is laid out

Igor Romcy
  • 336
  • 4
  • 7
1

When onMeasure is called the view gets its measured width/height. After this, you can call layout.getMeasuredHeight().

Dr.jacky
  • 3,341
  • 6
  • 53
  • 91
Pavan
  • 711
  • 2
  • 6
  • 17
0

I create the static variable to check if the layout was already drawn. If it was created you can simply call the function that draws.

static int firstAccess = 0;
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main8);

    if (firstAccess==0) {
        LAYOUT_YOU_DRAW.post(new Runnable() {
            @Override
            public void run() {
                creatSpread();
                firstAccess = 1;
            }
        });
    }else{
        creatSpread();
    }
}
Adrian
  • 1
  • 2
-1

view.post { TODO("Not yet implemented") }

  • 2
    As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community May 24 '22 at 08:58