onGlobalLayoutListener vs postRunnable - android

I am trying to do calculate view's x,y positions after completion of loading of activity. What I did is view.postDelayed(runnable, 2000) which is working fine. code reviewer is not happy with this and suggested to use OnGlobalLayoutListener to know about the completion of activity loading. Somehow I don't like OnGlobalLayoutListener because it is associated with entire view tree which is not required for my solution. I am trying to understand pros and cons of these approaches. Thanks!

If all you are trying to do is read the view's x and y coordinates, I recommend using view.post(Runnable) with no delay (unless there is a good reason to include a delay). This will add the Runnable to a message queue to in the UI thread. The Runnable will wait to execute until after your View is inflated and attached to the window. Since View position property values depend on the view's layout context, posting a Runnable will give you the timing that you are looking for.
As you mentioned in your question description, an OnGlobalLayoutListener will apply to the entire View's layout as the class name suggests. An OnGlobalLayoutListener should only be considered if you are concerned with the layout state or visibility of any or all views within the view tree. I.e. anything that causes the view tree to be re-laid out.

Code reviewer is not happy because you wait 2s and guess that the loading of the activity is finished by then. This may be the case with your emulator or device but on older and slower devices the activity may not have finished loading. To be 100% safe that the activity has finished loading you should use the listener to inform the 'listener' when loading is completed.

I think that the correct way of doing this is by adding an onPreDrawListener to the view. This is the listener that is called when the view is about to be drawn, and where you already have all the information about it's size (width and height) and position (X and Y).
Example:
final View v = new View(getContext());
v.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
#Override
public boolean onPreDraw() {
v.getViewTreeObserver().removeOnPreDrawListener(this);
float myX = v.getX();
float myY = v.getY();
return true;
}
});
Don't forget to make the method onPreDraw return true, I don't know why but Android Studio makes it return false when you create the listener.
Also don't forget to remove the listener from the view, otherwise it might be uselessly called again.

Related

Android - onGlobalLayout is not called by ViewTreeObserver

I have a headache in current Android project. I want to detect the changing of the current page. For example, there is a TextView to display device time, which is updated per second. How to detect this change? I searched a lot on SO (thanks SO), but none works for me.
More information: I don't use standard Activity to create page. My way is:
All widgets are created into a View object which is then used to create a container object. After that, I just handle this container to draw on a canvas with a VSYNC callback Choreographer.FrameCallback periodically.
Indeed, it works to draw. All are ok. Except: I want to draw canvas only when the page's content changed. So back to my beginning question, how to detect this "changing" event? I am sure there is some kind of callback to handle this problem. I used the following solution, but onGlobalLayout is not called when textview's text changed.
CanvasAppViewContainer container;//CanvasAppViewContainer extends AbsoluteLayout
LayoutInflater li =(LayoutInflater)getService().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = li.inflate(resourceId, null);//passed a correct the layout id
container = new CanvasAppViewContainer(getService(), view, getWidth(), getHeight(), getSurface());
rootView = (LinearLayout) findViewById(R.id.rootView); //root element of layout
rootView.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
#SuppressLint("NewApi")
#Override
public void onGlobalLayout() {
Log.d("test", "onGlobalLayout");
}
});
BTW: Even if I register the view tree observer for the textview, onGlobalLayout is still not called.
Thanks
onGlobalLayout() is only called when the layout changes. When you want to detect a change in the text use TextView.addTextChangedListener()

Posting a runnable to a View that invalidates the View sometimes doesn't work

I been fighting an odd issue these last few days. I have a custom ExpandableListAdapter where each row contains an ImageView, among other things. I have a class that handles the asynchronous loading of images from the multitude of places they may reside (disk cache, app data, remote server, etc). In my adapter's getView method I delegate the responsibility of returning a View to the list Item itself (I have multiple row types for my group list). I request the image load as follows:
final ImageView thumb = holder.thumb;
holder.token = mFetcher.fetchThumb(mImage.id, new BitmapFetcher.Callback() {
#Override
public void onBitmap(final Bitmap b) {
thumb.post(new Runnable() {
#Override
public void run() {
thumb.setImageBitmap(b);
}
});
}
#Override
public void onFailure() {
}
});
Yeah, it's ugly, but I decided against some contract where you have the BitmapFetcher.Callback execute its methods on the UI thread by default.
Anyway, when I load the Activity that contains the ExpandableListView there will often be thumb images missing from different rows in the list. Reloading the Activity may cause some of the missing thumbs to show but others that were previously showing may not be anymore. The behavior is pretty random as far as I can tell. Scrolling the ListView such that the rows with missing images get recycled causes the new thumb images (when the recycled row gets displayed again) to load fine. Scrolling back to rows that previously contained missing images causes the missing images to appear. I can confirm that all the images are loading correctly from my BitmapFetcher (mFetcher) class. I should also mention that I load other images in other places. Every once in awhile they don't appear either.
After pulling most of my hair out, I discovered that changing:
thumb.post(new Runnable() {
to:
mExpListView.post(new Runnable() {
fixes the issue. I originally thought that the issue might be happening because I was using a final reference to a View, but the other locations in the app use non-final references to a view to post messages, and, as I mentioned, sometimes those did not work. I eventually changed everything to use an Activity's runOnUiThread() method (and my own getUiThreadRunner().execute method when inside Fragments) and that seems to fix the issue all around.
So my question remains, in what cases can View.post() to fail to deliver the runnable to the associated ViewRoot's message queue in the proper order? Or, perhaps the invalidate() is happening before the View is returned from getView and thus before it's placed in a ViewGroup that can be reached from the root View. Those are really the only cases I can think of that would prevent the image from showing up. I can guarantee that none of these calls are happening until at least onStart has finished executing. Further, it looks like it's fine to post to a View even if it hasn't been attached to a Window yet:
// Execute enqueued actions on every traversal in case a detached view enqueued an action
getRunQueue().executeActions(attachInfo.mHandler);
(in performTraversal). The only difference between the runOnUiThread and post seems to be that an Activity has a different Handler than the ViewRootImpl.
Activity:
final Handler mHandler = new Handler();
whereas in ViewRootImpl:
final ViewRootHandler handler = new ViewRootHandler();
But, this should not be a problem provided both Handlers were constructed in the same Thread (or using the same Looper). That leaves me wondering if it is, indeed, a problem to invalidate() a View that has not yet been added to the hierarchy. For this to be the case invalidate should either 1. not do anything if it's not visible, or 2. only be valid for the next performTraversal() that happens.
View.invalidate() checks a nice private method that's not documented called skipInvalidate():
/**
* Do not invalidate views which are not visible and which are not running an animation. They
* will not get drawn and they should not set dirty flags as if they will be drawn
*/
private boolean skipInvalidate() {
return (mViewFlags & VISIBILITY_MASK) != VISIBLE && mCurrentAnimation == null &&
(!(mParent instanceof ViewGroup) ||
!((ViewGroup) mParent).isViewTransitioning(this));
}
It looks like number 1 is more accurate! However, I would think this only pertains to a View's VISIBILITY property. So, is it accurate to assume that a View is considered not VISIBLE if it cannot be reached from the ViewRoot? Or is the VISIBILITY property unaffected by the View's container? If the former is the case (which I suspect it is) it raises a concern. My use of Activity.runOnUiThread is not a solution to the problem. It only happens to work because the invalidate() calls are being sent to a different Handler and being executed later (after getView returns and after the row has been added and made visible on the screen). Has anybody else run into this issue? Is there a good solution?
Hey David I ran into a similar issue long time back. The basic requirement for view.post(Runnable r) is that the view should be attached to the window for Runnable to be executed. However, since you are loading images asynchronously in your first case, therefore there is a probability that imageView aren't attached to window when post request is made and hence, some images fail to load.
Quoting earlier version of docs on the same:
View.post() : Causes the Runnable to be added to the message queue. The runnable will be run on the user interface thread. This method can
be invoked from outside of the UI thread only when this View is
attached to a window.
Switching to you next question, what is the best solution to handle this situation ?
Can't comment on the best solution. However, I think both handler.post() and activity.runOnUIThread() are good to go. Since, they basically post runnable in main thread queue irrespective of anything and in general, the request to display list rows would be enqueued prior to our thumb.post(). So, they might work flawlessly for most cases. (Atleast I've never faced a problem with them !). However. if you find a better solution, do share it with me.
Try this : setBitmap() like this :
runOnUiThread(new Runnable() {
#Override
public void run() {
thumb.setImageBitmap(b);
}
});

Fragments : what is the best place to measure Views?

I have a fragment and I need to measure location/width/height of its views on screen and pass to some other class.
So what I have is a function which does it, something like this :
private void measureTest(){
v = ourView.findViewById(R.id.someTextField);
v.getLocationOnScreen(loc);
int w = v.getWidth();
...
SomeClass.passLocation(loc,w);
...
The problem is that the location/width/height of views is not ready within fragment lifecycle.
So if I run that function within these lifecycle methods :
onCreateView
onViewCreated
onStart
onResume
I either get wrong location and width/height measurments or 0 values.
The only solution I found is to add a GlobalLayoutListener like this to mainView
mainView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
public void onGlobalLayout() {
if(alreadyMeasured)
mainView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
else
measureTest();
}
});
this gets the job done.. but its just Yack! IMO.
Is there a better way of doing this? seems like such a basic thing to do
inside onActivityCreated of your fragment retrieve the currentView (with getView()) and post a runnable to its queue. Inside the runnable invoke measureTest()
There is no better way. That code isn't that bad! It's fired as soon as the view is layed out (my terminology might be a bit weird there) which happens right after measuring. That is how it is done in the BitmapFun sample (see ImageGridFragment, line 120) in Google's Android docs. There is a comment on that particular piece of code stating:
// This listener is used to get the final width of the GridView and then calculate the
// number of columns and the width of each column. The width of each column is variable
// as the GridView has stretchMode=columnWidth. The column width is used to set the height
// of each view so we get nice square thumbnails.

What triggers a View's measure() to be called

In my application I have an infinite loop on one of my View's onMeasure() overrides. Debugging the source code starting from a break point in my onMeasure, I am able to trace myself all the way up the stack trace up to the PhoneWindow$DecorView's measure() (top most class in my View Hierarchy), which gets called by ViewRoot.performTraversals(). Now from here if I keep stepping over, I eventually get the PhoneWindow$DecorView's measure() called again by a message in the Looper.loop() class. I'm guessing something has queued up a message that it needs to remeasure, like an invalidate.
My question is, what triggers that a measure call needs to occur on a View?
From my understanding of the layout/measure/draw process, this will only occur when the invalidate() method is called on a specific view, and that will trickle down and perform a layout/measure/draw pass for that view invalidated and all of its children. I would assume that somehow my top most View in my View Hierarchy is getting invalidated.
However, I've explicitly put a break point on every single invalidate call I have, and am not calling invalidate in some infinite manner. So I do not think that is the case. Is there another way to trigger a measure pass? Could something internally be triggering this? I'm kind of out of ideas after seeing that nothing is infinity invalidating.
In order to trigger a measure pass for a custom View you must call the requestLayout() method.
For example, if you are implementing a custom view that extends View and it will behave like a TextView, you could write a setText method like this:
/**
* Sets the string value of the view.
* #param text the string to write
*/
public void setText(String text) {
this.text = text;
//calculates the new text width
textWidth = mTextPaint.measureText(text);
//force re-calculating the layout dimension and the redraw of the view
requestLayout();
invalidate();
}
Well, If you are changing a View's content, it will eventually have to call invalidate(). For example, you have a TextView with a text called "Text 1". Now, you change the text of the same TextView to "Text 2". Here aslo, invalidate will be called.
So basically, when something changes on the view, more often than not, you would expect the invalidate method to be called, and a corresponding call to the measure().
Look at the source code for TextView, for example.
http://www.google.com/codesearch#uX1GffpyOZk/core/java/android/widget/TextView.java&q=TextView%20package:android&type=cs
Count the number of invalidate calls. There are quite a few.

Canvas do not update (invalidate) until whole while loop ends

I'm trying to move a ball on canvas. a and b are similar to x,y coordinate positions. Any way from my code im trying to get different values dynamically. The a,b are global variables. But it seems that "invalidate()" or the refreshing of screen only happens afer end of the whole loop. Do you know why?. And if i have to build this on another thread please suggest me with some simple codes.
private void shootBall(){
while (a>b){
a = getPositionX();
b = getPositionY();
invalidate();
}
}
}
I think it's more correct to say that you can call invalidate() from within a loop, but that that invalidation will not be handled (the canvas won't be redrawn) until after your loop is complete. The problem is that you are calling invalidate on the same thread (the UI toolkit thread) as the one that would call your onDraw() method. So unless/until you hand control back to the toolkit, it cannot possibly do the rendering. So your invalidate() call does actually invalidate the view ... but the view won't be redrawn until after your loop completes and your function returns.
It is more correct to change the position in some function that is called via some timer (which is essentially what the animation classes do). In that function, you would change the values, invalidate() appropriately, and return. Then the toolkit re-renders the scene and your function will get future callbacks and update the position accordingly.
do it like this, and use postInvalidate() instead:
private void shootBall(){
new Thread(new Runnable() {
public void run() {
while (a>b){
a = getPositionX();
b = getPositionY();
postInvalidate();
}
}
}).start();
}
edit: but as mentioned before, don't assume that the invalidate redraws the screen, it marks it as to-be-redrawn and the UI thread will get around to it.
You can put invalidate() at the end of onDraw() like in this example: How can I use the animation framework inside the canvas?
However this works well on some devices while bottlenecks and slows down on other.
To use a thread and SurfaceView go through all of these tutorials: http://www.droidnova.com/playing-with-graphics-in-android-part-i,147.html
UI cant be modified form any new thread..you should use invalidate() in the same thread where your view

Categories

Resources