I create a BottomSheetDialogFragment and I want to adjust it's maximum expanded height. How can I do that? I can retrieve the BottomSheetBehaviour but all I can find is a setter for the peek height but nothing for the expanded height.
public class DialogMediaDetails extends BottomSheetDialogFragment
{
#Override
public void setupDialog(Dialog dialog, int style)
{
super.setupDialog(dialog, style);
View view = View.inflate(getContext(), R.layout.dialog_media_details, null);
dialog.setContentView(view);
...
View bottomSheet = dialog.findViewById(R.id.design_bottom_sheet);
BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomSheet);
behavior.setPeekHeight(...);
// how to set maximum expanded height???? Or a minimum top offset?
}
}
EDIT
Why do I need that? Because I show a BottomSheet Dialog in a full screen activity and it looks bad if the BottomSheet leaves a space on top...
The height is being wrapped because the inflated view is added to the FrameLayout which has layout_height=wrap_content. See FrameLayout (R.id.design_bottom_sheet) at https://github.com/dandar3/android-support-design/blob/master/res/layout/design_bottom_sheet_dialog.xml.
The class below makes the bottom sheet full screen, background transparent, and fully expanded to the top.
public class FullScreenBottomSheetDialogFragment extends BottomSheetDialogFragment {
#CallSuper
#Override
public void onViewCreated(View view, #Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
ButterKnife.bind(this, view);
}
#Override
public void onStart() {
super.onStart();
Dialog dialog = getDialog();
if (dialog != null) {
View bottomSheet = dialog.findViewById(R.id.design_bottom_sheet);
bottomSheet.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
}
View view = getView();
view.post(() -> {
View parent = (View) view.getParent();
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) (parent).getLayoutParams();
CoordinatorLayout.Behavior behavior = params.getBehavior();
BottomSheetBehavior bottomSheetBehavior = (BottomSheetBehavior) behavior;
bottomSheetBehavior.setPeekHeight(view.getMeasuredHeight());
((View)bottomSheet.getParent()).setBackgroundColor(Color.TRANSPARENT)
});
}
}
--- EDIT Aug 30, 2018 ---
I realized a year later that the background was colored on the wrong view. This dragged the background along with the content while a user was dragging the dialog.
I fixed it so that the parent view of the bottom sheet is colored.
I found a much simpler answer; in your example where you obtain the FrameLayout for the bottom sheet using this code
View bottomSheet = dialog.findViewById(R.id.design_bottom_sheet);
you can then set the height on the layout params for that View to whatever height you want to set the expanded height to.
bottomSheet.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
BIG UPDATE
Avoiding duplicated code I'm giving a link to the full answer in where you can find all the explanations about how to get full behavior like Google Maps.
I want to adjust its maximum expanded height. How can I do that?
Both BottomSheet and BottomSheetDialogFragment use a BottomSheetBehavior that you can found in Support Library 23.x
That Java class has 2 different uses for mMinOffset, one of them is used to define the area of the parent it will use to draw his content (maybe a NestedScrollView). And the other use is for defining the expanded anchor point, I mean, if you slide it up to form STATE_COLLAPSEDit will animate your BottomSheetuntil he reached this anchor point BUT if you can still keep sliding up to cover all parent height (CoordiantorLayout Height).
If you took a look at BottomSheetDialog you will see this method:
private View wrapInBottomSheet(int layoutResId, View view, ViewGroup.LayoutParams params) {
final CoordinatorLayout coordinator = (CoordinatorLayout) View.inflate(getContext(),
android.support.design.R.layout.design_bottom_sheet_dialog, null);
if (layoutResId != 0 && view == null) {
view = getLayoutInflater().inflate(layoutResId, coordinator, false);
}
FrameLayout bottomSheet = (FrameLayout) coordinator.findViewById(android.support.design.R.id.design_bottom_sheet);
BottomSheetBehavior.from(bottomSheet).setBottomSheetCallback(mBottomSheetCallback);
if (params == null) {
bottomSheet.addView(view);
} else {
bottomSheet.addView(view, params);
}
// We treat the CoordinatorLayout as outside the dialog though it is technically inside
if (shouldWindowCloseOnTouchOutside()) {
final View finalView = view;
coordinator.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
if (isShowing() &&
MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_UP &&
!coordinator.isPointInChildBounds(finalView,
(int) event.getX(), (int) event.getY())) {
cancel();
return true;
}
return false;
}
});
}
return coordinator;
}
No idea which one of those 2 behaviors you want but if you need the second one follow those steps:
Create a Java class and extend it from CoordinatorLayout.Behavior<V>
Copy paste code from the default BottomSheetBehavior file to your new one.
Modify the method clampViewPositionVertical with the following code:
#Override
public int clampViewPositionVertical(View child, int top, int dy) {
return constrain(top, mMinOffset, mHideable ? mParentHeight : mMaxOffset);
}
int constrain(int amount, int low, int high) {
return amount < low ? low : (amount > high ? high : amount);
}
Add a new state
public static final int STATE_ANCHOR_POINT = X;
Modify the next methods: onLayoutChild, onStopNestedScroll, BottomSheetBehavior<V> from(V view) and setState (optional)
And here is how it looks like
[]
Its works for me. Add code on BottomSheetDialogFragment's onViewCreated() methode
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
view.viewTreeObserver.removeOnGlobalLayoutListener(this)
val dialog = dialog as BottomSheetDialog
val bottomSheet = dialog.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet) as FrameLayout?
val behavior = BottomSheetBehavior.from(bottomSheet!!)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
val newHeight = activity?.window?.decorView?.measuredHeight
val viewGroupLayoutParams = bottomSheet.layoutParams
viewGroupLayoutParams.height = newHeight ?: 0
bottomSheet.layoutParams = viewGroupLayoutParams
}
})
dialogView = view
}
Don't forget to remove viewTreeObserver.
override fun onDestroyView() {
dialogView?.viewTreeObserver?.addOnGlobalLayoutListener(null)
super.onDestroyView()
}
Get reference to sheet behavior,
private val behavior by lazy { (dialog as BottomSheetDialog).behavior }
turn fitToContents off and set expandedOffset to desired pixels.
behavior.isFitToContents = false
behavior.expandedOffset = 100
Kotlin
In my case I need to define a fixed height and I did the following:
val bottomSheet: View? = dialog.findViewById(R.id.design_bottom_sheet)
BottomSheetBehavior.from(bottomSheet!!).peekHeight = 250
this way you also have access to any property of the BottomSheetBehavior such as halfExpandedRatio
I would advise against using ids to find views. In the BottomSheetDialogFragment the dialog is a BottomSheetDialog which exposes the behavior for the bottom sheet. You can use that to set the peek height.
(dialog as BottomSheetDialog).behavior.peekHeight = ...
I'm using the BottomSheetBehavior from Google recently released AppCompat v23.2. The height of my bottom sheet depends on the content displayed inside of the bottom sheet (similar to the what Google does themselves in their Maps app).
It works fine with the data loaded initially, but my application changes the content displayed during runtime and when this happens the bottom sheet retains at it's old height, which either leads to unused space at the bottom or a cut of view.
Is there any way to inform the bottom sheet layout to recalculate the height used for expanded state (when height of the ViewGroup is set to MATCH_HEIGHT) or any way to manually set the required height?
EDIT: I also tried to manually call invalidate() on the ViewGroup and the parent of it but without any success.
I had the same problem with RelativeLayout as my bottom sheet. The height won't be recalculated. I had to resort to setting the height by the new recalculated value and call BottomSheetBehavior.onLayoutChild.
This is my temporary solution:
coordinatorLayout = (CoordinatorLayout)findViewById(R.id.coordinator_layout);
bottomSheet = findViewById(R.id.bottom_sheet);
int accountHeight = accountTextView.getHeight();
accountTextView.setVisibility(View.GONE);
bottomSheet.getLayoutParams().height = bottomSheet.getHeight() - accountHeight;
bottomSheet.requestLayout();
behavior.onLayoutChild(coordinatorLayout, bottomSheet, ViewCompat.LAYOUT_DIRECTION_LTR);
You can use BottomSheetBehavior#setPeekHeight for that.
FrameLayout bottomSheet = (FrameLayout) findViewById(R.id.bottom_sheet);
BottomSheetBehavior<FrameLayout> behavior = BottomSheetBehavior.from(bottomSheet);
behavior.setPeekHeight(newHeight);
This does not automatically move the bottom sheet to the peek height. You can call BottomSheetBehavior#setState to adjust your bottom sheet to the new peek height.
behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
Though the issue has been resolved in >=24.0.0 support library, if for some reason you still have to use the older version, here is a workaround.
mBottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
#Override
public void onStateChanged(#NonNull final View bottomSheet, int newState) {
bottomSheet.post(new Runnable() {
#Override
public void run() {
//workaround for the bottomsheet bug
bottomSheet.requestLayout();
bottomSheet.invalidate();
}
});
}
#Override
public void onSlide(#NonNull View bottomSheet, float slideOffset) {
}
});
For bottom sheet dialog fragment, read this: Bottom Sheet Dialog Fragment Expand Full Height
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
BottomSheetDialog dialog = (BottomSheetDialog) getDialog();
FrameLayout bottomSheet = dialog.findViewById(com.google.android.material.R.id.design_bottom_sheet);
BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomSheet);
behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
behavior.setPeekHeight(0);
}
I faced the same issue, when trying to update the peek height based on its contents, the height from a previous layout was found. This makes sense as the new layout had not taken place yet. By posting on the UI thread the layout height is calculated after the new layout, and another layout request is made to update the bottom sheet to the right height.
void show() {
setVisibility(View.VISIBLE);
post(new Runnable() {
#Override
public void run() {
mBottomSheetBehavior.setPeekHeight(findViewById(R.id.sheetPeek).getHeight());
requestLayout();
}
})
}
I was facing the same issue when I used a recyclerview inside a BottomSheet and the items changed dynamically. As #sosite has mentioned in his comment, the issue is logged and they have fixed it in the latest release.
Issue log here
Just update your design support library to version 24.0.0 and check.
I've followed #HaraldUnander advice, and it gave me an idea which has actually worked. If you run a thread (couldn't make it work with the post method as him) after the BottomSheetBehavior.state is set up programmatically to STATE_COLLAPSED, then you can already obtain the height of your views and set the peekHeight depending on it's content.
So first you set the BottomSheetBehavior:
BottomSheetBehavior.from(routeCaptionBottomSheet).state = BottomSheetBehavior.STATE_COLLAPSED
And then you set the peekHeight dynamically:
thread {
activity?.runOnUiThread {
val dynamicHeight = yourContainerView.height
BottomSheetBehavior.from(bottomSheetView).peekHeight = dynamicHeight
}
}
If using Java (I'm using Kotlin with Anko for threads), this could do:
new Thread(new Runnable() {
public void run() {
int dynamicHeight = yourContainerView.getHeight();
BottomSheetBehavior.from(bottomSheetView).setPeekHeight(dynamicHeight);
}
}).start();
Below code snippet helped me solve this issue where i am toggling between visibility of different views in layout and height is automatically changing for my bottom sheet.
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.your_bottom_sheet_layout, container, false)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState) as BottomSheetDialog
dialog.setContentView(R.layout.your_bottom_sheet_layout)
dialog.setOnShowListener {
val castDialog = it as BottomSheetDialog
val bottomSheet = castDialog.findViewById<View?>(R.id.design_bottom_sheet)
val behavior = BottomSheetBehavior.from(bottomSheet)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.setBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
if (newState == BottomSheetBehavior.STATE_DRAGGING) {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
}
override fun onSlide(bottomSheet: View, slideOffset: Float) {}
})
}
return dialog
}
I've been struggling with a problem similar to yours.
Manually setting the height of the bottomSheet was the solution for me.
Having a view viewA that has the BottomSheetBehaviour and a custom method modifyHeight() that modifies the height of the view:
viewA?.modifyHeight()
viewA?.measure(
MeasureSpec.makeMeasureSpec(
width,
MeasureSpec.EXACTLY
),
MeasureSpec.makeMeasureSpec(
0,
MeasureSpec.UNSPECIFIED
)
)
val layoutParams = LayoutParams(viewA.measuredWidth, viewA.measuredHeight)
val bottomSheet = BottomSheetBehavior.from(viewA)
layoutParams.behavior = bottomSheet
viewA.layoutParams = layoutParams
My layout would be something like:
<com.yourpackage.ViewA
android:id="#+id/viewA"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:behavior_peekHeight="50dp"
app:layout_behavior="#string/bottom_sheet_behavior" />
It is important to reuse the bottomSheetBehaviour of the old layoutParams because it contains the peekHeight and listeners you may have attached.
Here is the toggle button click listener I have implement to set pick height of bottom sheet with animation
FrameLayout standardBottomSheet = findViewById(R.id.standardBottomSheet);
BottomSheetBehavior<FrameLayout> bottomSheetBehavior = BottomSheetBehavior.from(standardBottomSheet);
btnToggleBottomSheet.setOnClickListener(new HPFM_OnSingleClickListener() {
#Override
public void onSingleClick(View v) {
if (bottomSheetBehavior.getPeekHeight() == 0) {
ObjectAnimator.ofInt(bottomSheetBehavior, "peekHeight", 200).setDuration(300).start();
}
else {
ObjectAnimator.ofInt(bottomSheetBehavior, "peekHeight", 0).setDuration(300).start();
}
}
});
I am creating all of the elements in my android project dynamically. I am trying to get the width and height of a button so that I can rotate that button around. I am just trying to learn how to work with the android language. However, it returns 0.
I did some research and I saw that it needs to be done somewhere other than in the onCreate() method. If someone can give me an example of how to do it, that would be great.
Here is my current code:
package com.animation;
import android.app.Activity;
import android.os.Bundle;
import android.view.animation.Animation;
import android.view.animation.LinearInterpolator;
import android.view.animation.RotateAnimation;
import android.widget.Button;
import android.widget.LinearLayout;
public class AnimateScreen extends Activity {
//Called when the activity is first created.
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LinearLayout ll = new LinearLayout(this);
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
layoutParams.setMargins(30, 20, 30, 0);
Button bt = new Button(this);
bt.setText(String.valueOf(bt.getWidth()));
RotateAnimation ra = new RotateAnimation(0,360,bt.getWidth() / 2,bt.getHeight() / 2);
ra.setDuration(3000L);
ra.setRepeatMode(Animation.RESTART);
ra.setRepeatCount(Animation.INFINITE);
ra.setInterpolator(new LinearInterpolator());
bt.startAnimation(ra);
ll.addView(bt,layoutParams);
setContentView(ll);
}
Any help is appreciated.
The basic problem is, that you have to wait for the drawing phase for the actual measurements (especially with dynamic values like wrap_content or match_parent), but usually this phase hasn't been finished up to onResume(). So you need a workaround for waiting for this phase. There a are different possible solutions to this:
1. Listen to Draw/Layout Events: ViewTreeObserver
A ViewTreeObserver gets fired for different drawing events. Usually the OnGlobalLayoutListener is what you want for getting the measurement, so the code in the listener will be called after the layout phase, so the measurements are ready:
view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
view.getHeight(); //height is ready
}
});
Note: The listener will be immediately removed because otherwise it will fire on every layout event. If you have to support apps SDK Lvl < 16 use this to unregister the listener:
public void removeGlobalOnLayoutListener (ViewTreeObserver.OnGlobalLayoutListener victim)
2. Add a runnable to the layout queue: View.post()
Not very well known and my favourite solution. Basically just use the View's post method with your own runnable. This basically queues your code after the view's measure, layout, etc. as stated by Romain Guy:
The UI event queue will process events in order. After
setContentView() is invoked, the event queue will contain a message
asking for a relayout, so anything you post to the queue will happen
after the layout pass
Example:
final View view=//smth;
...
view.post(new Runnable() {
#Override
public void run() {
view.getHeight(); //height is ready
}
});
The advantage over ViewTreeObserver:
your code is only executed once and you don't have to disable the Observer after execution which can be a hassle
less verbose syntax
References:
https://stackoverflow.com/a/3602144/774398
https://stackoverflow.com/a/3948036/774398
3. Overwrite Views's onLayout Method
This is only practical in certain situation when the logic can be encapsulated in the view itself, otherwise this is a quite verbose and cumbersome syntax.
view = new View(this) {
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
view.getHeight(); //height is ready
}
};
Also mind, that onLayout will be called many times, so be considerate what you do in the method, or disable your code after the first time
4. Check if has been through layout phase
If you have code that is executing multiple times while creating the ui you could use the following support v4 lib method:
View viewYouNeedHeightFrom = ...
...
if(ViewCompat.isLaidOut(viewYouNeedHeightFrom)) {
viewYouNeedHeightFrom.getHeight();
}
Returns true if view has been through at least one layout since it was
last attached to or detached from a window.
Additional: Getting staticly defined measurements
If it suffices to just get the statically defined height/width, you can just do this with:
View.getMeasuredWidth()
View.getMeasuredHeigth()
But mind you, that this might be different to the actual width/height after drawing. The javadoc describes the difference in more detail:
The size of a view is expressed with a width and a height. A view
actually possess two pairs of width and height values.
The first pair is known as measured width and measured height. These
dimensions define how big a view wants to be within its parent (see
Layout for more details.) The measured dimensions can be obtained by
calling getMeasuredWidth() and getMeasuredHeight().
The second pair is simply known as width and height, or sometimes
drawing width and drawing height. These dimensions define the actual
size of the view on screen, at drawing time and after layout. These
values may, but do not have to, be different from the measured width
and height. The width and height can be obtained by calling getWidth()
and getHeight().
We can use
#Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
//Here you can get the size!
}
You are calling getWidth() too early. The UI has not been sized and laid out on the screen yet.
I doubt you want to be doing what you are doing, anyway -- widgets being animated do not change their clickable areas, and so the button will still respond to clicks in the original orientation regardless of how it has rotated.
That being said, you can use a dimension resource to define the button size, then reference that dimension resource from your layout file and your source code, to avoid this problem.
I used this solution, which I think is better than onWindowFocusChanged(). If you open a DialogFragment, then rotate the phone, onWindowFocusChanged will be called only when the user closes the dialog):
yourView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
// Ensure you call it only once :
yourView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
// Here you can get the size :)
}
});
Edit : as removeGlobalOnLayoutListener is deprecated, you should now do :
#SuppressLint("NewApi")
#SuppressWarnings("deprecation")
#Override
public void onGlobalLayout() {
// Ensure you call it only once :
if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
yourView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
else {
yourView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
// Here you can get the size :)
}
If you need to get width of some widget before it is displayed on screen, you can use getMeasuredWidth() or getMeasuredHeight().
myImage.measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
int width = myImage.getMeasuredWidth();
int height = myImage.getMeasuredHeight();
As Ian states in this Android Developers thread:
Anyhow, the deal is that layout of the
contents of a window happens
after all the elements are constructed and added to their parent
views. It has to be this way, because
until you know what components a View
contains, and what they contain, and
so on, there's no sensible way you can
lay it out.
Bottom line, if you call getWidth()
etc. in a constructor, it will return
zero. The procedure is to create all
your view elements in the constructor,
then wait for your View's
onSizeChanged() method to be called --
that's when you first find out your
real size, so that's when you set up
the sizes of your GUI elements.
Be aware too that onSizeChanged() is
sometimes called with parameters of
zero -- check for this case, and
return immediately (so you don't get a
divide by zero when calculating your
layout, etc.). Some time later it
will be called with the real values.
I would rather use OnPreDrawListener() instead of addOnGlobalLayoutListener(), since it is called a bit earlier than other listeners.
view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener()
{
#Override
public boolean onPreDraw()
{
if (view.getViewTreeObserver().isAlive())
view.getViewTreeObserver().removeOnPreDrawListener(this);
// put your code here
return true;
}
});
Adjusted the code according to comment of #Pang. onPreDraw method should return true to proceed with the current drawing pass.
AndroidX has multiple extension functions that help you with this kind of work, inside androidx.core.view
You need to use Kotlin for this.
The one that best fits here is doOnLayout:
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.
In your example:
bt.doOnLayout {
val ra = RotateAnimation(0,360,it.width / 2,it.height / 2)
// more code
}
Dependency: androidx.core:core-ktx:1.0.0
A Kotlin Extension to observe on the global layout and perform a given task when height is ready dynamically.
Usage:
view.height { Log.i("Info", "Here is your height:" + it) }
Implementation:
fun <T : View> T.height(function: (Int) -> Unit) {
if (height == 0)
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
viewTreeObserver.removeOnGlobalLayoutListener(this)
function(height)
}
})
else function(height)
}
It happens because the view needs more time to be inflated. So instead of calling view.width and view.height on the main thread, you should use view.post { ... } to make sure that your view has already been inflated. In Kotlin:
view.post{width}
view.post{height}
In Java you can also call getWidth() and getHeight() methods in a Runnable and pass the Runnable to view.post() method.
view.post(new Runnable() {
#Override
public void run() {
view.getWidth();
view.getHeight();
}
});
One liner if you are using RxJava & RxBindings. Similar approach without the boilerplate. This also solves the hack to suppress warnings as in the answer by Tim Autin.
RxView.layoutChanges(yourView).take(1)
.subscribe(aVoid -> {
// width and height have been calculated here
});
This is it. No need to be unsubscribe, even if never called.
Maybe this helps someone:
Create an extension function for the View class
filename: ViewExt.kt
fun View.afterLayout(what: () -> Unit) {
if(isLaidOut) {
what.invoke()
} else {
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
viewTreeObserver.removeOnGlobalLayoutListener(this)
what.invoke()
}
})
}
}
This can then be used on any view with:
view.afterLayout {
do something with view.height
}
Height and width are zero because view has not been created by the time you are requesting it's height and width .
One simplest solution is
view.post(new Runnable() {
#Override
public void run() {
view.getHeight(); //height is ready
view.getWidth(); //width is ready
}
});
This method is good as compared to other methods as it is short and crisp.
If you are using Kotlin
customView.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
customView.viewTreeObserver.removeOnGlobalLayoutListener(this)
}
else {
customView.viewTreeObserver.removeGlobalOnLayoutListener(this)
}
// Here you can get the size :)
viewWidth = customView.width
}
})
Answer with post is incorrect, because the size might not be recalculated.
Another important thing is that the view and all it ancestors must be visible. For that I use a property View.isShown.
Here is my kotlin function, that can be placed somewhere in utils:
fun View.onInitialized(onInit: () -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (isShown) {
viewTreeObserver.removeOnGlobalLayoutListener(this)
onInit()
}
}
})
}
And the usage is:
myView.onInitialized {
Log.d(TAG, "width is: " + myView.width)
}
For Kotlin:
I have faced a production crash due to use view.height/ view.width which lead to NaN while I was using View.post() which sometimes view diemsions returned with 0 value.
So,
Use view.doOnPreDraw { // your action here} which is:
OneShotPreDrawListener so it called only one time.
Implements OnPreDrawListener which make sure view is layouted and measured
well , you can use addOnLayoutChangeListener
you can use it in onCreate in Activity or onCreateView in Fragment
#Edit
dont forget to remove it because in some cases its trigger infinite loop
myView.addOnLayoutChangeListener(object : View.OnLayoutChangeListener{
override fun onLayoutChange(
v: View?, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int
) {
if (v?.width > 0 && v?.height > 0){
// do something
Log.i(TAG, "view : ${view.width}")
// remove after finish
v?.removeOnLayoutChangeListener(this)
}
}
})
Cleanest way of doing this is using post method of view :
kotlin:
view.post{
var width = view.width
var height = view.height
}
Java:
view.post(new Runnable() {
#Override
public void run() {
int width = view.getWidth();
int height = view.getHeight();
}
});
Gone views returns 0 as height if app in background.
This my code (1oo% works)
fun View.postWithTreeObserver(postJob: (View, Int, Int) -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
val widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
val heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
measure(widthSpec, heightSpec)
postJob(this#postWithTreeObserver, measuredWidth, measuredHeight)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
#Suppress("DEPRECATION")
viewTreeObserver.removeGlobalOnLayoutListener(this)
} else {
viewTreeObserver.removeOnGlobalLayoutListener(this)
}
}
})
}
We need to wait for view will be drawn. For this purpose use OnPreDrawListener. Kotlin example:
val preDrawListener = object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
view.viewTreeObserver.removeOnPreDrawListener(this)
// code which requires view size parameters
return true
}
}
view.viewTreeObserver.addOnPreDrawListener(preDrawListener)
In my case, I can't get a view's height by post or by addOnGlobalLayoutListener, it's always 0. Because my view is in a fragment, and the fragment is the second tab in MainActivity. when I open MainActivity, I enter the first tab, so the second tab doesn't show on the screen. But onGlobalLayout() or post() function still has a callback.
I get the view's height when the second fragment is visible on the screen. And this time I get the correct height.
Usage:
imageView.size { width, height ->
//your code
}
View extention:
fun <T : View> T.size(function: (Int, Int) -> Unit) {
if (isLaidOut && height != 0 && width != 0) {
function(width, height)
} else {
if (height == 0 || width == 0) {
var onLayoutChangeListener: View.OnLayoutChangeListener? = null
var onGlobalLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
onGlobalLayoutListener = object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (isShown) {
removeOnLayoutChangeListener(onLayoutChangeListener)
viewTreeObserver.removeOnGlobalLayoutListener(this)
function(width, height)
}
}
}
onLayoutChangeListener = object : View.OnLayoutChangeListener {
override fun onLayoutChange(
v: View?,
left: Int,
top: Int,
right: Int,
bottom: Int,
oldLeft: Int,
oldTop: Int,
oldRight: Int,
oldBottom: Int
) {
val width = v?.width ?: 0
val height = v?.height ?: 0
if (width > 0 && height > 0) {
// remove after finish
viewTreeObserver.removeOnGlobalLayoutListener(onGlobalLayoutListener)
v?.removeOnLayoutChangeListener(this)
function(width, height)
}
}
}
viewTreeObserver.addOnGlobalLayoutListener(onGlobalLayoutListener)
addOnLayoutChangeListener(onLayoutChangeListener)
} else {
function(width, height)
}
}
}
public final class ViewUtils {
public interface ViewUtilsListener {
void onDrawCompleted();
}
private ViewUtils() {
}
public static void onDraw(View view, ViewUtilsListener listener) {
view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
if (view.getHeight() != 0 && view.getWidth() != 0) {
if (listener != null) {
listener.onDrawCompleted();
}
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
}
});
}
}
you can use like this ;
ViewUtils.onDraw(view, new ViewUtils.ViewUtilsListener() {
#Override
public void onDrawCompleted() {
int width = view.getWidth();
int height = view.getHeight();
}
});
private val getWidth: Int
get() {
if (Build.VERSION.SDK_INT >= 30) {
val windowMetrics =windowManager.currentWindowMetrics
val bounds = windowMetrics.bounds
var adWidthPixels = View.width.toFloat()
if (adWidthPixels == 0f) {
adWidthPixels = bounds.width().toFloat()
}
val density = resources.displayMetrics.density
val adWidth = (adWidthPixels / density).toInt()
return adWidth
} else {
val display = windowManager.defaultDisplay
val outMetrics = DisplayMetrics()
display.getMetrics(outMetrics)
val density = outMetrics.density
var adWidthPixels = View.width.toFloat()
if (adWidthPixels == 0f) {
adWidthPixels = outMetrics.widthPixels.toFloat()
}
val adWidth = (adWidthPixels / density).toInt()
return adWidth
}
}
replace (View) with the view you want to measure
This is a little old, but was having trouble with this myself (needing to animate objects in a fragment when it is created). This solution worked for me, I believe it is self explanatory.
class YourFragment: Fragment() {
var width = 0
var height = 0
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val root = inflater.inflate(R.layout.fragment_winner_splash, container, false)
container?.width.let {
if (it != null) {
width = it
}
}
container?.height.let {
if (it != null) {
height = it
}
}
return root
}
If you're worried about overworking the onDraw method, you can always set the dimension as null during construction and then only set the dimension inside of onDraw if it's null.
That way you're not really doing any work inside onDraw
class myView(context:Context,attr:AttributeSet?):View(context,attr){
var height:Float?=null
override fun onDraw(canvas:Canvas){
if (height==null){height=this.height.toFloat()}
}
}