23

I'm trying to get the height and width of an ImageView in a Fragment with the following ViewTreeObserver:

import android.view.ViewTreeObserver;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;

private ImageView imageViewPicture;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_general_activity_add_recipe, container, false);
    setHasOptionsMenu(true);

    ...

    final ViewTreeObserver observer = imageViewPicture.getViewTreeObserver();
    observer.addOnGlobalLayoutListener (new OnGlobalLayoutListener () {
        @Override public void onGlobalLayout() {
            observer.removeGlobalOnLayoutListener(this);
        }
    });

    return view;
}

Running this code results in the following Exception:

10-12 23:45:26.145: E/AndroidRuntime(12592): FATAL EXCEPTION: main
10-12 23:45:26.145: E/AndroidRuntime(12592): java.lang.IllegalStateException: This ViewTreeObserver is not alive, call getViewTreeObserver() again
10-12 23:45:26.145: E/AndroidRuntime(12592):     at android.view.ViewTreeObserver.checkIsAlive(ViewTreeObserver.java:509)
10-12 23:45:26.145: E/AndroidRuntime(12592):     at android.view.ViewTreeObserver.removeGlobalOnLayoutListener(ViewTreeObserver.java:356)
10-12 23:45:26.145: E/AndroidRuntime(12592):     at com.thimmey.rezepte.AddRecipeActivity_GeneralFragment$1.onGlobalLayout(AddActivity_GeneralFragment.java:83)
10-12 23:45:26.145: E/AndroidRuntime(12592):     at android.view.ViewTreeObserver.dispatchOnGlobalLayout(ViewTreeObserver.java:566)
10-12 23:45:26.145: E/AndroidRuntime(12592):     at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1736)
10-12 23:45:26.145: E/AndroidRuntime(12592):     at android.view.ViewRootImpl.handleMessage(ViewRootImpl.java:2644)
10-12 23:45:26.145: E/AndroidRuntime(12592):     at android.os.Handler.dispatchMessage(Handler.java:99)
10-12 23:45:26.145: E/AndroidRuntime(12592):     at android.os.Looper.loop(Looper.java:137)
10-12 23:45:26.145: E/AndroidRuntime(12592):     at android.app.ActivityThread.main(ActivityThread.java:4517)
10-12 23:45:26.145: E/AndroidRuntime(12592):     at java.lang.reflect.Method.invokeNative(Native Method)
10-12 23:45:26.145: E/AndroidRuntime(12592):     at java.lang.reflect.Method.invoke(Method.java:511)
10-12 23:45:26.145: E/AndroidRuntime(12592):     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:993)
10-12 23:45:26.145: E/AndroidRuntime(12592):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:760)
10-12 23:45:26.145: E/AndroidRuntime(12592):     at dalvik.system.NativeStart.main(Native Method)

The documentations says that removeGlobalOnLayoutListener is deprecated but if I use removeOnGlobalLayoutListener, as suggested, I get an undefined error.

What I am doing wrong?

TWiStErRob
  • 44,762
  • 26
  • 170
  • 254
float
  • 1,265
  • 5
  • 22
  • 38

3 Answers3

73

try this :

   ViewTreeObserver observer = imageViewPicture.getViewTreeObserver();
   observer.addOnGlobalLayoutListener (new OnGlobalLayoutListener () {
    @Override
     public void onGlobalLayout() {

       imageViewPicture.getViewTreeObserver().removeGlobalOnLayoutListener(this);
      }
    });
ρяσѕρєя K
  • 132,198
  • 53
  • 198
  • 213
  • 2
    Thank You! Now I also can use the suggested removeOnGlobalLayoutListener. – float Oct 12 '12 at 22:20
  • 3
    But I've seen lots of code where the observer is stored in a local variable - and that worked. Don't understand why in many cases this is valid, but not in all? Do you know?? – Zordid Mar 16 '14 at 10:03
  • Aren't these two observers different? So the original would still have the listener. – Danyal Aytekin May 20 '14 at 13:18
  • @Zordid, those cases must be later in the lifecycle when the view is already attached to the hierarchy, see ["But it works sometimes!"](http://stackoverflow.com/a/29172475/253468). – TWiStErRob Mar 20 '15 at 17:30
  • @DanyalAytekin, they are different, but one is the superset of the other, see ["Are they the same observers?"](http://stackoverflow.com/a/29172475/253468). – TWiStErRob Mar 20 '15 at 17:31
  • 2
    `removeGlobalOnLayoutListener` is deprecated. Any Suggestion? – Pratik Butani Nov 10 '15 at 06:12
20

The other solution will work just fine, but it doesn't explain why this is happening.

There are a few issues to address here:

GlobalOn VS OnGlobal

Using the GlobalOn version will give you a deprecation warning, but if you check the source it's just calling the OnGlobal version, so they're equivalent. The difference is that you can only use the OnGlobal one from API Level 16, so if you're targeting an earlier version you'll have to use GlobalOn and deal with that deprecation warning.

Why is it dead?

Note that this question is about code in onCreateView, where imageViewPicture is not attached to the view hierarchy yet, it has just been inflated. If you take a quick look at View.getViewTreeObserver() you can see that it creates a "floating" observer in this case. Then when the view is put in the hierarchy dispatchAttachedToWindow is called which then merges the window's and the view's floating observers:

info.mTreeObserver.merge(mFloatingTreeObserver);

which moves all registered listeners to the window's observer and kills the floating observer.

The original observer you acquired is the floating one, which is dead, hence calling an add/remove on it results in the above exception.

Are they the same observers?

As Danyal, I was also puzzled why removeGlobalOnLayoutListener works on a different observer, but now it's clear with the temporary floating observer. As the floating one is merged to the window's observer the listeners are moved to the other observer, so calling View.getViewTreeObserver() later will give you an observer containing your listener. The new observer is now responsible for handling your listener.

But it works sometimes!, and another solution

As to Zordid's comment on why in many cases it's ok to hold on in a local (closure) variable can be explained by a similar reasoning: the just inflated view in onCreateView is not yet attached only a little after it's returned. Most of the you've seen is probably in a method after onCreateView in the lifecycle. float's (OP) solution would work just fine if the observer related code was in onViewCreated. Every lifecycle method has it's own responsibilities, so I would advise splitting the code up like this:

private ImageView imageViewPicture;

@Override public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setHasOptionsMenu(true);
}

@Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_general_activity_add_recipe, container, false);

    // assuming ... includes:
    this.imageViewPicture = view.findViewById(R.id.image);

    return view;
}

@Override public void onViewCreated(View view, Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);

    final ViewTreeObserver observer = imageViewPicture.getViewTreeObserver();
    observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener () {
        @Override public void onGlobalLayout() {
            observer.removeGlobalOnLayoutListener(this);
        }
    });
}

I also like to wire my other listeners in onViewCreated, this way the number of instance variables are minimized, so imageViewPicture would be a local variable.

The same is probably true for Activity.setContentView which attaches the inflated view immediately and is usually called in onCreate so the hierarchy is live by the time you play with observers/listeners.

Community
  • 1
  • 1
TWiStErRob
  • 44,762
  • 26
  • 170
  • 254
  • Kudos for the detailed analysis, but I'm sorry to tell you that you're wrong about your advice with registering the listener in `onViewCreated` and using a local final var `final ViewTreeObserver observer`. I tested it on Nexus 6 (Android 5.1) and I constantly get an exception that the observer is not alive. The only way it works is according to @ρяσѕρєя K solution. – Nimrod Dayan Jun 16 '15 at 16:02
  • @CodePond.org thanks for pointing that out. I'll look into when I go back to developing Android again. Just curious: are you using a static or a dynamic fragment? Shouldn't make a difference but who knows... I also did all testing on 4.4.2, we'll see. – TWiStErRob Jun 16 '15 at 16:19
  • 1
    Excellent answer. I just want to point out a small mistake why this fail to work for @NimrodDayan. `final ViewTreeObserver observer = imageViewPicture.getViewTreeObserver();` will assign `observer` to the floating `ViewTreeObserver`. After `dispatchAttachedToWindow` is called, this floating observer will be merged to window's `ViewTreeObserver` and killed which is why there is an exception on `observer` not alive. Calling `imageViewPicture.getViewTreeObserver()` every time should be the solution and returns the correct (merged or floating) `ViewTreeObserver`. – Weizhi Nov 03 '17 at 06:05
3

Better try to check if a getViewTreeObserver is alive. I think the following code will work. Based on https://stackoverflow.com/a/15301092/2914140, https://stackoverflow.com/a/26193736/2914140 and some others.

** Update **

After reading Remove listener from ViewTreeObserver, https://stackoverflow.com/a/40013262/2914140 I rewrote a bit.

public static void captureGlobalLayout(@NonNull final View view,
                                       @NonNull final ViewTreeObserver.OnGlobalLayoutListener listener) {
    ViewTreeObserver vto = view.getViewTreeObserver();
    if (vto.isAlive()) {
        vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                    @Override
                    public void onGlobalLayout() {
                        ViewTreeObserver vto = view.getViewTreeObserver();
                        if (vto.isAlive()) {
                            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                                vto.removeOnGlobalLayoutListener(this);
                            } else {
                                //noinspection deprecation
                                vto.removeGlobalOnLayoutListener(this);
                            }
                            listener.onGlobalLayout();
                        }
                    }
                });
    } else {
        view.post(new Runnable() {
            @Override
            public void run() {
                listener.onGlobalLayout();
            }
        });

    }
}

** Old answer **

Strange but even if view.getViewTreeObserver().isAlive() is true, then next call to view.getViewTreeObserver() may again produce the same exception. So, I surrounded a code with try-catch. Possibly you may skip all this and retain only view.post(...) block.

if (view.getViewTreeObserver().isAlive()) {
    try {
        view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                if (view.getViewTreeObserver().isAlive()) {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                        view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                    } else {
                        view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                    }
                }
                yourCode();
            }
        });
    } catch (IllegalStateException e) {
        e.printStackTrace();
        // The same as below branch.
        getActivity().runOnUiThread(new Runnable() {
            @Override
            public void run() {
                view.post(new Runnable() {
                    @Override
                    public void run() {
                        yourCode();
                    }
                });
            }
        });
    }
} else {
    getActivity().runOnUiThread(new Runnable() {
        @Override
        public void run() {
            // Try to wait until the view is ready.
            view.post(new Runnable() {
                @Override
                public void run() {
                    yourCode();
                }
            });
        }
    });
}

Probably view.post(...) is enough, but I called it from background thread, so if you do the same, better should invoke it from runOnUiThread(new Runnable() ....

Community
  • 1
  • 1
CoolMind
  • 26,736
  • 15
  • 188
  • 224