I need to call requestLayout() in my custome view, but I noticed
This should not be called while the view hierarchy is currently in a layout pass ({#link #isInLayout()}.
So I deciede to use this code:
if(isInLayout()) {
// request layout later
} else {
requestLayout();
}
But the question is that I don't know how to request layout later, can I use addOnLayoutChangeListener ?
just like this:
addOnLayoutChangeListener(new OnLayoutChangeListener() {
#Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
requestLayout();
}
});
If you want to request a new layout after the layout pass that is in progress completes, take a look at ViewTreeObserver.OnPreDrawListener and the PreDraw() method.
onPreDraw
boolean onPreDraw ()
Callback method to be invoked when the view tree is about to be drawn. At this point, all views in the tree have been measured and given a frame. Clients can use this to adjust their scroll bounds or even to request a new layout before drawing occurs.
There are other methods that are part of the ViewTreeObserver.OnPreDrawListener interface that may also be what you are looking for.
You can also look at post() that is part of View. (See documentation here). I believe that the Runnable that you post will be executed after the layout is completed on the view. That may be more of what you are looking for. Also take a look at the accepted answer to this Stack Overflow question.
Related
Let's say we have a main_activity.xml layout that defines all dimensions in a relative manner -- constraints, percentages, and guidelines (that are percentages)... no "static" dp.
But in MainActivity.java, we programatically create some subviews, and we want to define their height/width dimensions as relative to existing views.
We do not know the dimensions or density of the device so, so nor do we know the (actual integer) dimensions of any view before run-time...
But we can say something like:
int heightDimensionForNewView = (int) (someAlreadyInflatedView.getHeight() / 7f)
But what if, under certain circumstances, these "new" views need to be displayed immediately at app start-time?
So, the question:
In the Android Activity life-cycle, when is the earliest point at which you can (somehow) safely query (something) for actual/finalized/guaranteed layout dimensions? And what is that something and somehow?
I haven't been able to find an override method such as "onContentViewInflated()" and there is no onCreateView() method like there is in Fragments.
I've also tried Logging from inside onStart() and onResume() but the dimension results are always "0," presumably because they haven't been inflated yet.
I know that any given View can get its own dimensions in onMeasure(), but then you would have make a static variable in MainActivity in order to assign it and use it from there... or some way of sending that information from the View back to the Activity.
What am I missing? I just want to be able to get the number somehow from inside MainActivity itself.
My suggestions are:
view.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
#Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
int oldWidth = oldRight - oldLeft; // right exclusive, left inclusive
if( v.getWidth() != oldWidth ) {
// width has changed
}
}
});
and
view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
// View has laid out
// Remove the layout observer if you don't need it anymore
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
});
Without resorting to the infamous onGlobalLayoutListener() solution, and without having to implement a custom View, what lifecycle event in a Fragment can I put code into and be sure all of the Fragment's Views have been given a size?
As a corollary, I would also like this lifecycle event to be applicable to Fragments in a ViewPager.
I dont think there would be any Fragment lifecycle event to be sure all the Views have size.
What I would usually do is, to use OnLayoutChangeListener inside onActivityCreated(). Like this,
getView().addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
#Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
getView().removeOnLayoutChangeListener(this);
// Check the size of Views here.
}
});
From my understanding, there's no API for developers to determine when AdapterView's are getting redrawn.
We call notifyDataSetChanged() and then, at some point in the future, with no event for us to listen for, the ListView redraws it's views.
I say this because I've encountered a situation where I am updating images in a ListView when the scroll has stopped.
Every time I set a new list source - i.e. call notifyDataSetChanged() from my adapter, I then call my updateImagesInView() method - kind of like this:
//MyListView.java
public void setDataSource(SomeClass dataSource) {
((MyListAdapter)myListView.getAdapter()).setSomeDataSource(dataSource);
updateImagesInView();
}
public void updateImagesInView() {
for (int i = 0; i <= mListView.getLastVisiblePosition() - mListView.getFirstVisiblePosition(); i++) {
View listItemView = mListView.getChildAt(i);
...
}
}
//MyListAdapater.java
public void setSomeDataSource(SomeClass dataSource) {
mDataSource = dataSource;
notifyDataSetChanged();
}
The child views I get from the loop in the updateImagesInView method always belong to the previous dataSource.
I've hacked in a workaround, so I'm not looking for a "how to do this" answer, but more along the lines of - is there anyway to know when the views in a ListView have actually been updated after calling notifyDataSetChanged()? (or am I just doing something crazy wrong because the views should effectively be updated immediately after calling notifyDataSetChanged()?)
Well you can add a listener to yourListView's layout like:
mListView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
#Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
mListView.removeOnLayoutChangeListener(this);
Log.e(TAG, "updated");
}
});
mAdapter.notifyDataSetChanged();
Otherwise you should listen on your adapter, as when notifyDataSetChanged is called, your adapter gets calls to getView() to update all the views that are currently visible.
In a Fragment, I am inflating a Layout with multiple child View. I need to get the dimensions (width and height) of one of them which is a custom view.
Inside the custom view class I can do it easily. But if I try to do it from the fragment I always get 0 as dimensions.
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
View culoide = view.findViewWithTag(DRAW_AREA_TAG);
Log.d("event", "culoide is: "+culoide.getWidth()); // always 0
}
I figure that onViewCreated should be the right place to get it, but well this happens. I tried before super.onViewCreated, in debug it looks like 'findViewWithTag' finds the right view, tried with api 7 v4 support only.
Any help?
You must wait until after the first measure and layout in order to get nonzero values for getWidth() and getHeight(). You can do this with a ViewTreeObserver.OnGlobalLayouListener
public void onViewCreated(final View view, Bundle saved) {
super.onViewCreated(view, saved);
view.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
public void onGlobalLayout() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
} else {
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
// get width and height of the view
}
});
}
My preferred method is to add an OnLayoutChangeListener to the view that you want to track itself
CustomView customView = ...
customView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
#Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
// Make changes
}
});
You can remove the listener in the callback if you only want the initial layout.
Using ViewTreeObserver.OnGlobalLayoutListener, View.post(Runnable action) or onWindowFocusChanged() isn't the best solution. This article (note: I am the author of this article) explains why and provides a working solution using doOnLayout kotlin extension, which is based on View.OnLayoutChangeListener. If you want it in Java, in the article there's a link to doOnLayout source code, it's very simple and you can do something similar in Java too.
You have to wait until the onSizeChanged() method is called before you can reliably determine the View size.
This is called during layout when the size of this view has changed.
If you were just added to the view hierarchy, you're called with the
old values of 0.
Try calling
culoide.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
first, then try getWidth() and getHeight()
Try checking in onWindowFocusChanged and it should have valid values:
public void onWindowFocusChanged (boolean hasFocus) { }
I had a similar issue where I needed to get width and height of a widget and this was the function in which I could guarantee the widget reported it's correct size.
this is a real pain especially because you expected with a name like onViewCreated in fragments lifecycle that the view is ready. for me get the fragment view itself like this:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
getView()?.let{
it.doOnLayout{// do your UI work here }
}
}
this ensures the fragments getView has actually had one layout pass already.
Dianne Hackborn mentioned in a couple threads that you can detect when a layout as been resized, for example, when the soft keyboard opens or closes. Such a thread is this one... http://groups.google.com/group/android-developers/browse_thread/thread/d318901586313204/2b2c2c7d4bb04e1b
However, I didn't understand her answer: "By your view hierarchy being resized with all of the corresponding layout traversal and callbacks."
Does anyone have a further description or some examples of how to detect this? Which callbacks can I link into in order to detect this?
Thanks
One way is View.addOnLayoutChangeListener. There's no need to subclass the view in this case. But you do need API level 11. And the correct calculation of size from bounds (undocumented in the API) can sometimes be a pitfall. Here's a correct example:
view.addOnLayoutChangeListener( new View.OnLayoutChangeListener()
{
public void onLayoutChange( View v,
int left, int top, int right, int bottom,
int leftWas, int topWas, int rightWas, int bottomWas )
{
int widthWas = rightWas - leftWas; // Right exclusive, left inclusive
if( v.getWidth() != widthWas )
{
// Width has changed
}
int heightWas = bottomWas - topWas; // Bottom exclusive, top inclusive
if( v.getHeight() != heightWas )
{
// Height has changed
}
}
});
Another way (as dacwe answers) is to subclass your view and override onSizeChanged.
Override onSizeChanged in your View!
With Kotlin extensions:
inline fun View?.onSizeChange(crossinline runnable: () -> Unit) = this?.apply {
addOnLayoutChangeListener { _, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
val rect = Rect(left, top, right, bottom)
val oldRect = Rect(oldLeft, oldTop, oldRight, oldBottom)
if (rect.width() != oldRect.width() || rect.height() != oldRect.height()) {
runnable();
}
}
}
Use thus:
myView.onSizeChange {
// Do your thing...
}
My solution is to add an invisible tiny dumb view at the end of of the layout / fragment (or add it as a background), thus any change on size of the layout will trigger the layout change event for that view which could be catched up by OnLayoutChangeListener:
Example of adding the dumb view to the end of the layout:
<View
android:id="#+id/theDumbViewId"
android:layout_width="1dp"
android:layout_height="1dp"
/>
Listen the event:
View dumbView = mainView.findViewById(R.id.theDumbViewId);
dumbView.addOnLayoutChangeListener(new OnLayoutChangeListener() {
#Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
// Your code about size changed
}
});
Thank to https://stackoverflow.com/users/2402790/michael-allan this is the good and simple way if you don't want to override all your views.
As Api has evolved I would suggest this copy paste instead:
String TAG="toto";
///and last listen for size changed
YourView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
#Override
public void onLayoutChange(View v,
int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
boolean widthChanged = (right-left) != (oldRight-oldLeft);
if( widthChanged )
{
// width has changed
Log.e(TAG,"Width has changed new width is "+(right-left)+"px");
}
boolean heightChanged = (bottom-top) != (oldBottom-oldTop);
if( heightChanged)
{
// height has changed
Log.e(TAG,"height has changed new height is "+(bottom-top)+"px");
}
}
});
View.doOnLayout(crossinline action: (view: View) -> Unit): Unit
See docs.
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.
Kotlin version base on #Michael Allan answer to detect layout size change
binding.root.addOnLayoutChangeListener { view, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
if(view.height != oldBottom - oldTop) {
// height changed
}
if(view.width != oldRight - oldLeft) {
// width changed
}
}
Other answers are correct, but they didn't remove OnLayoutChangeListener. So, every time this listener was called, it added a new listener and then called it many times.
private fun View.onSizeChange(callback: () -> Unit) {
addOnLayoutChangeListener(object : 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)
if (right - left != oldRight - oldLeft || bottom - top != oldBottom - oldTop) {
callback()
}
}
})
}
If your like me and you came to this page and
addOnLayoutChangeListener gave you functionality you needed but caused an infinite list of "requestLayout() improperly called .... running second layout pass" messages to fill your logcat and ...
Adding removingOnLayoutChangeListener(this) broke the functionality you had in 1) despite fixing the repeated logcat message then this might work.
Its a mix of what Slion and Tony suggested.
Create a ResetDetectorCallback interface with abstract method
resizeAction.
Create a simple custom view ResetDetector with an instance of the ResetDetectorCallback, a setResetDetectorCallback mehtod, and override its onSizeChange() method to invok the resetDectectorCallback.resizeAction().
In the layout place an
instance of the ResetDetectorView with an id, width="match_parent", height="match_parent", and in my case I also constrained it.
Then in the code (mine was a Fragment since I'm adopting the Single Activity methodology) let that class implement the ResetDetectorCallback, set the resetDetectorView from that class's layout to use "this", and implement the resizeAction in that class.
And in my case its finally working the way I want now. On a Desktop emulator I can resize that Window as much as I want and it appears tobe accurately adjusting the layout accordingly to the way I want and the logcat is not being overloaded with "requestLayout() improperly called .... running second layout pass" messages.
There is probably a better way, perhaps with ViewModels and mutableStateObservers, but I'm not familiar enough with those yet. I hope this helps someone somewhere. And thanks to all the contributors above even if your technique didn't work for me.