Android - onGlobalLayout is not called by ViewTreeObserver - android

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()

Related

onGlobalLayoutListener vs postRunnable

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.

Get on Screen Position of Objects in OnCreate()

I am facing following problem:
I got 4 Buttons over the screen, which can be dragged and move back to their original position when dropping the button.
To do so I want to read out the specific position of the button (on the specific device) by using the getX() and getY() function and writing the results into an array.
buttons[0][0] = button.getX();
buttons[0][1] = button.getY();
As I cannot use these functions in the onCreate or in the onStart (the position is always (0,0) cause the view is not instantiated yet I am thinking about how to read out the original position without any extra coding.
Thanks a lot
In your onCreate—though onResume may make more sense—get a reference to a view that is an ancestor of all the buttons (or just the root of the activity, which may be easiest) and then do this:
rootView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
rootView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
// Read values here.
buttons[0][0] = button.getX();
buttons[0][1] = button.getY();
}
});
not sure what you mean by "extra coding" but you could use an OnTouchListener (or modify the one you're already using, if that's the case) to capture the position as soon as they touch the button, but before they drag it anywhere (I think that would be ACTION_DOWN).
This has the advantage of only being set when you need it to, skipping it when they don't actually drag the buttons, and not calling it for every layout pass, etc.

Show views currently focused (Even without layout selectors)

I'm trying to inspect a code for a very big Android (Amazon Fire TV) activity but i keep loosing the focus in the running app and i don't know what element is being focused.
I'm looking for a way (Wether it's an App, a developer setting - Show Layout Limits gets near - or something i can code inside the activity) to see what view is being focused, without having to change the layout (Selectors) of every single view.
What do you suggest?
Activity has a method called getCurrentFocus().
Maybe you could call hasFocus() on all the Views if the above doesn't work. I imagine the method would look something like this:
public View getFocusedView(View layout)
{
View focusedView = null;
// Note: I'm not sure if FOCUS_DOWN is the right one to use here
// so you may want to see the other constants offered
ArrayList<View> views = layout.getFocusables(View.FOCUS_DOWN)
for(View v: views)
{
if(v.hasFocus())
{
focusedView = v;
}
}
return focusedView;
}

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.

Trying to understand view hierarchy

I'm writing an app with the sole purpose of trying to understand how the view hierarchy in Android works. I am having some really harsh problems in it right now. I'll try to be concise in my explanation here.
Setup:
Currently I have three Views. 2 are ViewGroups and 1 is just a View. Let's say they're in this order:
TestA extends ViewGroup
TestB extends ViewGroup
TestC extends View
TestA->TestB->TestC
Where TestC is in TestB and TestB is in TestA.
In my Activity I simply display the views like so:
TestA myView = new TestA(context);
myView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT));
setContentView(myView);
Problems:
The onDraw(Canvas canvas) method of TestA is never called. I've seen a couple solutions to this saying that my view doesn't have any dimensions (height/width = 0), however, this is not the case. When I override onLayout(), I get the dimensions of my layout and they are correct. Also, getHeight()/Width() are exactly as they should be. I can also override dispatchDraw() and get my base views to draw themselves.
I want to animate an object in TestB. Traditionally, I would override the onDraw() method on call invalidate() on itself until the object finish the animation it was supposed to do. However, in TestB, when I call invalidate() the view never gets redrawn. I'm under the impression that it's the job of my parent view to call the onDraw() method again, but my parent view does not call the dispatchDraw() again.
I guess my questions are, why would my onDraw() method of my parent view never get called to begin with? What methods in my parent view are supposed to be called when one of it's children invalidate itself? Is the parent the one responsible for ensure it's children get drawn or does Android take care of that? If Android responds to invalidate(), why does my TestB never get drawn again?
Ok, after some research and a lot of trying, I've found out I was doing three things wrong in regards to problem #2. A lot of it was simple answers but not very obvious.
You need to override onMeasure() for every view. Within onMeasure(), you need to call measure() for every child contained in the ViewGroup passing in the MeasureSpec that the child needs.
You need to call addView() for every child you want to include. Originally, I was simply created a view object and using it directly. This allowed me to draw it once, but the view was not include in the view tree, thus it when I called invalidate() it wasn't invalidating the view tree and not redrawing. For example:
In testA:
TestB childView;
TestA(Context context){
****** setup code *******
LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
childView = new TestB(context);
childView.setLayoutParams(params);
}
#Override
protected void dispatchDraw(Canvas canvas){
childView.draw(canvas);
}
This will draw the child view once. However, if that view needs updating for animations or whatever, that was it. I put addView(childView) in the constructor of TestA to add it to the view tree. Final code is as such:
TestB childView;
TestA(Context context){
****** setup code *******
LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
childView = new TestB(context);
childView.setLayoutParams(params);
addView(childView);
}
#Override
protected void dispatchDraw(Canvas canvas){
childView.draw(canvas);
}
Alternatively, I could override dispatchDraw(Canvas canvas) like so if I had many more children to draw, but I need some custom element between each drawing like grid lines or something.
#Override
protectd void dispatchDraw(Canvas canvas){
int childCount = getChildCount();
for(int i = 0; i < size; i++)
{
drawCustomElement();
getChildAt(i).draw(canvas);
}
}
You must override onLayout() (it's abstract in ViewGroup anyway, so it's required anyway). Within the this method, you must call layout for every child. Even after doing the first two things, my views wouldn't invalidate. As soon as I did this, everything worked just perfectly.
UPDATE:
Problem #1 has been solved. Another extremely simply but not-so-obvious solution.
When I create an instance of TestA, I have to call setWillNotDraw(false) or else Android will not draw it for optimization reasons. So the full setup is:
TestA myView = new TestA(context);
myView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT));
myView.setWillNotDraw(false);
setContentView(myView);
This isn't a direct answer, but here is a fantastic tutorial on how to draw custom views and gives a great crash cours in how to animate a view. The code is very simple and clean too.
Hope this helps!

Categories

Resources