Skip to content

Measuring views in Android from onCreate fails

In my latest Android project I am using a ListView to display data, which consists of TextViews and ImageViews.
The bitmaps for the ImageViews are loaded asynchronously from the Internet. Since the data source for the ListView adapter currently holds about 300 items, and is probably growing in the future I don’t want the images to load on every scroll movement of the list but rather only when the list finishes moving. ListView offers two methods to query the visible items:

getFirstVisiblePosition()

and

getLastVisiblePosition()

I use these methods in the OnScroll event listener

@Override

public void onScrollStateChanged(AbsListView view, int scrollState) {

   switch (scrollState) {

   case SCROLL_STATE_IDLE:

      Log.d(LOG_TAG, "scrolling stoped : visible range = " + listView.getFirstVisiblePosition() + " to "

        + listView.getLastVisiblePosition());

      loadImages();

      break;
   }
}

This works perfectly when the user scrolls the ListView but the problem is the initial setup of the ListView. The ListView, and its adapter are initialized in the onCreate method. But at that point of time the ListView is not fully setup, i.e. it has not been completely drawn and attached to its parent window and getLastVisiblePosition() returns -1.

A lot of threads on the Internet cover this topic and most of them recommend using a view’s ViewTreeObserver that is used to register listeners that can be notified of global changes in the view tree.

The problem with the ViewTreeObserver is that the listener can be called multiple times by the system because the layout tree can change until it reaches its final state.
Since I don’t want my image load method to be invoked multiple times the ViewTreeObserver is not an option for me.

Others advise to use a Runnable, which queries the visible positions from the ListView. The way to do that is to use a handler that posts the Runnable to the UI thread. A handler interacts with a threads message looper (a class that runs a message loop for a thread), by sending and obtaining Message objects or Runnables. The corresponding thread is always the thread from where the handler is created. Since I create the handler in the onCreate method it is automatically associated with the main thread (UI thread).

The problem is that this solution does not work, either. The reason is that by the time the runnable is run by the main thread the ListView has yet not finished rendering its items and getLastVisiblePosition() still returns -1. The solution to this problem is to use the ListView’s own post method instead of a handler’s. Every view (that extends View) inherits the View class post method, which interacts with the main looper.

But why does

listView.post(getVisibleItems);

work while

handler.post(getVisibleItems);

does not?

The answer lies in the post method of the View class.

/**
* <p>Causes the Runnable to be added to the message queue.
* The runnable will be run on the user interface thread.</p>
*
* @param action The Runnable that will be executed.
*
* @return Returns true if the Runnable was successfully placed in to the
*         message queue.  Returns false on failure, usually because the
*         looper processing the message queue is exiting.
*
* @see #postDelayed
* @see #removeCallbacks
*/

public boolean post(Runnable action) {

   final AttachInfo attachInfo = mAttachInfo;

   if (attachInfo != null) {
      return attachInfo.mHandler.post(action);
   }

   // Assume that post will succeed later

   ViewRootImpl.getRunQueue().post(action);

   return true;

}

The important part is the mAttachInfo. AttachInfo is a data structure that is initialized when a view is attached to its parent window. Since the ListView is not attached to its parent window when we post the Runnable in onCreate, the first if-statement is not executed and the Runnable is routed further to the run queue of the ViewRootImpl.

ViewRootImpl is the top of a view hierarchy, implementing the needed protocol between a view and the WindowManager. The comments for the RunQueue class states:

“The run queue is used to enqueue pending work from Views when no Handler is attached.  The work is executed during the next call to performTraversals on
the thread.”

performTraversals is the method inside the ViewRootImpl class responsible for measuring, layouting and drawing a view.

So to cut a long story short the main difference between a handler’s post method and a View’s is the fact that the handler just posts the Runnable to the main looper where it is executed as soon as possible (after onCreate and after onResume) even if the view has not been attached to its parent window yet.

A view’s post method checks whether the view is attached to a window. If not (meaning the view does not have a valid handler to the main loop), the Runnable is routed further to a point where the view has been drawn and a handler exists. Detail information like the visible items of a ListView can then be read.

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

*