Android Expandable RecyclerView different Card height - java

I have a RecyclerView that contains a list of cards, each of which expand into child cards.
Each card has different text. I want that when the user clicks on a child card, it will expand to show the text inside. The expansion height is based on how much text the card contains.
I tried to measure the target height by using:
view.Measure(ViewGroup.LayoutParams.WrapContent, ViewGroup.LayoutParams.WrapContent);
And then expanding the card to the Measured Height (see here).
However, it gives the same Measured Height to all of the cards.
Here is my code, which is based on this (more specifically the Xamarin version):
This is the main Adapter, which creates and binds the parent and the child cards:
public class HalachaExpandableAdapter : ExpandableRecyclerAdapter<HalachaParentViewHolder, HalachaChildViewHolder>, View.IOnClickListener
{
LayoutInflater _inflater;
bool expand;
int targetHeight;
bool wave = false;
public HalachaExpandableAdapter(Context context, List<IParentObject> itemList) : base(context, itemList)
{
_inflater = LayoutInflater.From(context);
}
public override void OnBindChildViewHolder(HalachaChildViewHolder childViewHolder, int position, object childObject)
{
var halachaChild = (HalachaChild)childObject;
childViewHolder.halachaChildTitle.Text = halachaChild.Title.ToString();
targetHeight = childViewHolder.halachaChildCard.Height;
childViewHolder.halachaChildCard.LayoutParameters.Height = 100;
childViewHolder.halachaChildCard.SetOnClickListener(this);
expand = childViewHolder.expand;
}
public override void OnBindParentViewHolder(HalachaParentViewHolder parentViewHolder, int position, object parentObject)
{
var halacha = (HalachaItem)parentObject;
parentViewHolder._halachaTitleTextView.Text = halacha.Title();
parentViewHolder._halachaContentTextView.Text = halacha.Content;
if (halacha.ChildObjectList.Count == 1)
wave = true;
}
public void OnClick(View v)
{
if (v.Height == 100)
{
AnimationCollapse anim = new AnimationCollapse(v, targetHeight, 100);
anim.Duration = 300;
v.StartAnimation(anim);
expand = false;
}
else
{
AnimationCollapse anim = new AnimationCollapse(v, 100, v.Height);
anim.Duration = 300;
v.StartAnimation(anim);
expand = true;
}
}
public override HalachaChildViewHolder OnCreateChildViewHolder(ViewGroup childViewGroup)
{
var view = _inflater.Inflate(Resource.Layout.halachotListItem, childViewGroup, false);
return new HalachaChildViewHolder(view);
}
public override HalachaParentViewHolder OnCreateParentViewHolder(ViewGroup parentViewGroup)
{
var view = _inflater.Inflate(Resource.Layout.halachotListHeader, parentViewGroup, false);
wave = false;
return new HalachaParentViewHolder(view);
}
}
I think this is where the code is needed to be done, but if you need some of the other code of the other classes, I will gladly post them. You can also look at the links above for reference to how this works.
Hope someone can help me.
Thanks!

I finally managed to solve the problem.
I needed to change the way I measured the view's wrap_content height to this:
v.Measure(MeasureSpec.MakeMeasureSpec(v.Width, MeasureSpecMode.Exactly), MeasureSpec.MakeMeasureSpec(ViewGroup.LayoutParams.WrapContent, MeasureSpecMode.AtMost));
The rest is the same.
Thanks to this post.

I dont know why do you want to find the height of the card , instead you can just wrap the child cards height to that of text and make child card invisible , as soon as user click a card , child card s visiblity will be turned to visible and card wih its text with the same height will appear !

Related

Remove gestures from Android Q

I have a kids drawing app, but the gestures in Q keep quitting the app. I tried removing the system gesture but it does not seem to work.
In this case, I am trying to exclude the whole screen from system gesture:
List<Rect> exclusionRects = new ArrayList();
public void onLayout(boolean changedCanvas, int left, int top, int right, int bottom) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
exclusionRects.clear();
exclusionRects.add(new Rect(left, top, right, bottom));
setSystemGestureExclusionRects(exclusionRects);
}
}
As stated by Google:
First, to ensure reliable and consistent operation, there’s a 200dp vertical app exclusion limit for the Back gesture.
https://android-developers.googleblog.com/2019/08/final-beta-update-official-android-q.html
This means that the operating system will not allow you to override the back gesture fully.
This makes sense as it is a fairly fundamental part of the operating system and they probably don't want to allow apps that remove the gesture entirely, as it is bad for consistency across the platform
Try this.
Define this code in your Utils class.
static List<Rect> exclusionRects = new ArrayList<>();
public static void updateGestureExclusion(AppCompatActivity activity) {
if (Build.VERSION.SDK_INT < 29) return;
exclusionRects.clear();
Rect rect = new Rect(0, 0, SystemUtil.dpToPx(activity, 16), getScreenHeight(activity));
exclusionRects.add(rect);
activity.findViewById(android.R.id.content).setSystemGestureExclusionRects(exclusionRects);
}
public static int getScreenHeight(AppCompatActivity activity) {
DisplayMetrics displayMetrics = new DisplayMetrics();
activity.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
int height = displayMetrics.heightPixels;
return height;
}
public static int dpToPx(Context context, int i) {
return (int) (((float) i) * context.getResources().getDisplayMetrics().density);
}
Check if your layout is set in that activity where you want to exclude the edge getures and then apply this code.
// 'content' is the root view of your layout xml.
ViewTreeObserver treeObserver = content.getViewTreeObserver();
treeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
content.getViewTreeObserver().removeOnGlobalLayoutListener(this);
SystemUtil.updateGestureExclusion(MainHomeActivity.this);
}
});
We are adding the view width to 16dp to trigger the code when user swipe right from left edge & height to screen height to do it fully left side.

OnClick in one RecyclerView item affects other items

Edit #1: Through debugging I've discovered that the bug 'disappears'. Basically I set a breakpoint and slowly go through steps of checking each multiChoiceItem and the heights of the other RecyclerView child items do not change. Does this mean it is a drawing/timing related issue?
Edit #2: Also, a new find, if I change the height of Child: 6 it changes for Child: 3 and Child: 0
I apologize for the long question. I've checked other answers regarding the same problem and none of them apply. I've tried solving this myself and just couldn't so I would love some help. If there is anything I can do to make this easier to read, please let me know and I'll get right on it!
With the way my code is written, this technically should be impossible to happen but yet here it is.
The Problem: I have an onClickListener() for a TextView within a RecyclerView item. The onClickListener() calls a multiChoiceItem AlertDialog in the container class of the RecyclerAdapter which then calls notifyDataSet(), after completed, with an addOnLayoutChangeListener() at the end which measures the height after the new RecyclerView is drawn.
Notifying that the data set ended then causes the TextView within the RecyclerView item to change to show the text of each Checked item. Then this height is measured in the addOnLayoutChangeListener() and sent to a ViewModel which measures the height of the same position item of three fragments and sets the items height to the max height so they all look the same height.
The Confusing Part: This problem only occurs for one of the three fragments AND the other effected item heights do not match the other two fragments. Which tells me that this is localized to one fragment (which has its own class)
The Code:
The code is long so I reduced it to what I think was important
The ViewHolder
class TextViewViewHolder extends RecyclerView.ViewHolder {
TextView vhTVTextView;
TextView vhTVMainTextView;
CardView vhTVCardView;
TextViewClickedListener vhTextViewClickedListener;
// Gets current position from 'onBindViewHolder'
int vhPosition = 0;
public TextViewViewHolder(View itemView, TextViewClickedListener textViewClickedListener) {
super(itemView);
this.vhTextViewClickedListener = textViewClickedListener;
this.vhTVCardView = itemView.findViewById(R.id.thoughtCard);
this.vhTVTextView = itemView.findViewById(R.id.thoughtNumber);
this.vhTVMainTextView = itemView.findViewById(R.id.textEntry);
/*
When the main TextView is clicked, it calls a function in the container
'FragTextView' which pops up an AlertDialog. It was chosen to do it in the
container instead of here because the Adapter is so adapt the lists data to the view
and the container is what dictates what the lists data actually is.
*/
vhTVMainTextView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if(vhTextViewClickedListener != null) {
vhTextViewClickedListener.onTextViewClicked(vhPosition);
}
}
});
}
}
onBindViewHolder
#Override
public int getItemViewType(int position) {
/*
If mThoughtEntries is not null, then that means we can find the ViewType we are working
with inside of it. Otherwise, we are mDistortions and we must be working on TYPE_TEXTVIEW
*/
if(mThoughtEntries != null) return mThoughtEntries.get(position).getViewType();
else return Constants.TYPE_TEXTVIEW;
}
#Override
public void onBindViewHolder(#NonNull RecyclerView.ViewHolder holder, int position) {
int adapterPosition = holder.getAdapterPosition();
switch (holder.getItemViewType()) {
case Constants.TYPE_EDITTEXT:
EditTextViewHolder editTextViewHolder = (EditTextViewHolder)holder;
// update MyCustomEditTextListener every time we bind a new item
// so that it knows what item in mDataset to update
editTextViewHolder.mMyCustomEditTextListener.setTWPosition(holder.getAdapterPosition());
//Displaying list item to its correct position
editTextViewHolder.vhETTextView.setText(String.valueOf(adapterPosition + 1));
editTextViewHolder.vhETEditText.setText(mThoughtEntries.get(adapterPosition).getThought());
break;
case Constants.TYPE_TEXTVIEW:
TextViewViewHolder textViewViewHolder = (TextViewViewHolder)holder;
// Send current position to viewHolder so when the text listener is called, it knows
// exactly which position of the Distortions list to change
textViewViewHolder.vhPosition = adapterPosition;
//Displaying list item to its correct position
textViewViewHolder.vhTVTextView.setText(String.valueOf(adapterPosition + 1));
textViewViewHolder.vhTVMainTextView.setText(distortionsToString(mDistortions.get(adapterPosition)));
break;
}
}
AlertDialog in Parent
#Override
public void onTextViewClicked(int position) {
//pass the 'context' here
AlertDialog.Builder alertDialog = new AlertDialog.Builder(getContext());
final int recyclerPosition = position;
/*
Turning the distortions into a list of strings and an array of what should, or should
not, be checked.
*/
final String[] distortionStrings = distortionNameToStringArray(mDistortions.get(position));
final boolean[] checkedDistortions = distortionCheckToBooleanArray(mDistortions.get(position));
alertDialog.setMultiChoiceItems(distortionStrings, checkedDistortions,
new DialogInterface.OnMultiChoiceClickListener() {
#Override
public void onClick(DialogInterface dialog, int which, boolean isChecked) {
if (isChecked) {
// If the user checked the item, add it to the selected items
mDistortions.get(recyclerPosition).get(which).setChecked(true);
} else {
// Else, if the item is already in the array, remove it
mDistortions.get(recyclerPosition).get(which).setChecked(false);
}
/*
Because the RecyclerView takes a while to draw, if we call the below function
as we normally we would, it would appear to have no effect because it would
be automatically overwritten when the RecyclerView is drawn. So we call this
onLayout change listener to wait til the view is drawn and then we call
the function
*/
mRecyclerView.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) {
mRecyclerView.removeOnLayoutChangeListener(this);
// Send new height to the ViewModel
if(mLayoutManager.findViewByPosition(recyclerPosition) != null) {
// Get view of item measuring
View recyclerChild = mLayoutManager.findViewByPosition(recyclerPosition);
// Get LinearLayout from view
LinearLayout linearLayout = recyclerChild.findViewById(R.id.horizontalLayout);
// This is called to find out how big a view should be. The constraints are to check
// measurement when it is set to 'wrap_content'.
linearLayout.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
// Get height of the specified view
int height = linearLayout.getMeasuredHeight();
// Send to child abstracted class which then calls function from 'SharedEntryFragments'
setViewModelHeight(height, recyclerPosition);
}
}
});
mAdapter.notifyDataSetChanged();
}
});
alertDialog.setPositiveButton("Okay", new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) {
// DO SOMETHING HERE
dialog.cancel();
}
});
AlertDialog dialog = alertDialog.create();
dialog.show();
}
The function that makes all the fragment item heights equal
I know this part of the code doesn't affect it because where the views that heights are changed are skipped by if(positionalHeight.get(i) != 0) {} So technically...they should never change!
/*
This is the listener that will set all the RecyclerViews childrens heights. It
listens to getTallestLiveHeight() inside of 'SharedEntryFragments.java' and when
a change occurs, this is called
*/
if(getActivity() != null) {
// The container holds the ViewModel so this must make sure getActivity() is not null
mViewModel = ViewModelProviders.of(getActivity()).get(SharedEntryFragments.class);
/*
Creates the observer which updates the UI. The observer takes the
PositionalHeight class as an input. This class keeps track of which index
of the RecyclerView to change and what height it will be changed to.
*/
final Observer<List<Integer>> maxHeight = new Observer<List<Integer>>() {
#Override
public void onChanged(#Nullable final List<Integer> positionalHeight) {
if (positionalHeight != null) {
// Get the index that we are going to change and its height
//int position = positionalHeight.getPosition();
//int height = positionalHeight.getHeight();
/*
We're going to run through each child of mRecyclerView and change
its height accordingly
*/
int listSize = positionalHeight.size();
for(int i = 0; i < listSize; i++) {
// If height reads zero then skip because it will make our view disappear
if(positionalHeight.get(i) != 0) {
// This is the child item that we will be changing
View recyclerChild = mLayoutManager.findViewByPosition(i);
// Ensure that the child exists before continuing
if (recyclerChild != null) {
// We will be changing the CardView's height
// TODO might have to add a check to detect which viewholder
CardView cardView = recyclerChild.findViewById(R.id.thoughtCard);
// Get the LayoutParams first to ensure everything stays the same
ViewGroup.LayoutParams lparams = cardView.getLayoutParams();
// Get and set height
lparams.height = positionalHeight.get(i);
cardView.setLayoutParams(lparams);
}
}
}
}
}
};
mViewModel.getTallestLiveHeight().observe(this, maxHeight);
}
}
I wish I could provide a better answer for other people but this is what I discovered:
For some reason when I call mAdapter.notifyDataSetChanged(); in the AlertDialog function, every third item in the RecyclerView changed to the equaled height. I decided to change it to mAdapter.notifyItemChanged(recyclerPosition); to save on memory and, coincidentally, the bug has disappeared.
If someone could explain why, I will set that as the accepted answer but as of now, this satisfies the question so I will keep it as an answer.

Android View Disappearing When Go Outside Of Parent

I have a LinearLayout and ImageView inside this LinearLayout.
There is a translation effect for ImageView.
// v = ImageView
ObjectAnimator animation2 = ObjectAnimator.ofFloat(v, "translationY", 200);
animation2.setDuration(3000);
animation2.setTarget(v);
animation2.start();
Animation working but it's disappearing when ImageView go outside of LinearLayout.
How can i fix it without modify LinearLayout's height.
Find the ViewGroup that the ImageView belongs to and apply ViewGroup.setClipChildren(false).
By default, the drawing of the children is limited to the bounds of the parent ViewGroup.
Two attributes exist that may cause this to happen: clipChildren and clipToPadding. You'll need to set clipChildren to false for each parent ViewGroup whose bounds the object will animate out of. You also need to set clipToPadding to the immediate parent (and maybe more, but I haven't seen a case for it yet).
You can set both attributes in the XML
android:clipChildren="false"
android:clipToPadding="false"
or in code
viewGroup.setClipChildren(false);
viewGroup.setClipToPadding(false);
My implementation. It can probably help somebody:
Java version:
public static void setAllParentsClip(View v, boolean enabled) {
while (v.getParent() != null && v.getParent() instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) v.getParent();
viewGroup.setClipChildren(enabled);
viewGroup.setClipToPadding(enabled);
v = viewGroup;
}
}
call
setAllParentsClip(yourView, false); to disable the clipping in all the parents.
Edited:
Kotlin's version as an extension function:
fun View.setAllParentsClip(enabled: Boolean) {
var parent = parent
while (parent is ViewGroup) {
parent.clipChildren = enabled
parent.clipToPadding = enabled
parent = parent.parent
}
}
Call: yourView.setAllParentsClip(false)
In my case clipChildren did nothing but clipToPadding="false" fixed the problem. Go figure.
Get the view height, then add a percentage of the height to where it will slide to
public void SlideUp(View view){
float height = view.getHeight();
TranslateAnimation animate = new TranslateAnimation(0,0,0,0);
animate.setDuration(500);
animate.setFillAfter(true);
view.animate().translationY((float)(0-0.62*height)).start();
view.startAnimation(animate);
}
try to update camera position as in my case below:
ValueAnimator lockAnimator = ValueAnimator.ofFloat(1, 0); // value from 0 to 1
lockAnimator.setDuration(500);
lockAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
#Override
public void onAnimationUpdate(ValueAnimator pAnimation) {
float value = (Float) (pAnimation.getAnimatedValue());
if (value < .6 && flipped) {
if (preview != null)
mCanvasImage.setImageBitmap(preview);
else
mCanvasImage.setImageBitmap(imageBitmap);
flipped = false;
}
if (value > .3 && value < .7) {
lyt_rlt_container.setCameraDistance(lyt_rlt_container.getCameraDistance() - 100);
} else {
lyt_rlt_container.setCameraDistance(lyt_rlt_container.getCameraDistance() + 100);
}
lyt_rlt_container.setRotationY(180 * value);
}
});
lockAnimator.start();

android get adMob banner height when appears

I am adding an adMob banner to my app successfully. When banner appears I need to get its height in order to resize all layout elements. I am using the event onReceivedAd, that is properly fired. However, alturaBanner is = 0. Then, how to get its height? thank you.
/** Called when an ad is received. */
#Override
public void onReceiveAd(Ad ad)
{
adView.setVisibility(View.VISIBLE);
int alturaBanner = adView.getHeight();
RelativeLayout.LayoutParams params1 = (android.widget.RelativeLayout.LayoutParams) browse2
.getLayoutParams();
params1.setMargins(0, alturaBanner, 0, 0);
Log.d(LOG_TAG, "onReceiveAd");
Toast.makeText(this, "onReceiveAd", Toast.LENGTH_SHORT).show();
}
You can get the height of any type of banner before it is even added to the layout.
int heightPixels = AdSize.SMART_BANNER.getHeightInPixels(this);
or
int heightPixels = AdSize.FULL_BANNER.getHeightInPixels(myContext);
or for DIP's
int heightDP = AdSize.BANNER.getHeight();
So for your need, you could do this:
/** Called when an ad is received. */
#Override
public void onReceiveAd(Ad ad)
{
adView.setVisibility(View.VISIBLE);
int alturaBanner = AdSize.BANNER.getHeight(); // This gets the adsize, even if the view is not inflated.
RelativeLayout.LayoutParams params1 = (android.widget.RelativeLayout.LayoutParams) browse2
.getLayoutParams();
params1.setMargins(0, alturaBanner, 0, 0);
Log.d(LOG_TAG, "onReceiveAd");
Toast.makeText(this, "onReceiveAd", Toast.LENGTH_SHORT).show();
}
Just change AdSize.BANNER to AdSize.SMART_BANNER or whatever banner type your using.
Add Sizes Get Height
getting the height of the view before it was prepared will always return you 0 .
use the next code in order to get its correct size , no matter which device/screen you have:
private static void runJustBeforeBeingDrawn(final View view, final Runnable runnable)
{
final ViewTreeObserver vto = view.getViewTreeObserver();
final OnPreDrawListener preDrawListener = new OnPreDrawListener()
{
#Override
public boolean onPreDraw()
{
runnable.run();
final ViewTreeObserver vto = view.getViewTreeObserver();
vto.removeOnPreDrawListener(this);
return true;
}
};
vto.addOnPreDrawListener(preDrawListener);
}
inside the given runnable , you can query the real size of the view.
alternatively , you can use addOnGlobalLayoutListener instead of addOnPreDrawListener if you wish.
another approach is to use onWindowFocusChanged (and check that hasFocus==true) , but that's not always the best way ( only use for simple views-creation, not for dynamic creations)
EDIT: Alternative to runJustBeforeBeingDrawn: https://stackoverflow.com/a/28136027/878126
I use the following method to get AdView's height:
adView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
int height = adView.getHeight();
if (height > 0) {
// now the height is gotten, you can do things you want
}
}
});
onGlobalLayout() is triggered when the global layout state or the visibility of views within the view tree changes.
Actually, you don't need to wait for appearance of adview to get adMob banner height if you are using smart banner type for showing banner ads.
Use Smart banner as it automatically decides the height of ad based on device size. Use full width of screen to show the ad.
From android developers site:
Smart Banners are ad units that will render screen-wide banner ads on any screen size across different devices in either orientation. Smart Banners help deal with increasing screen fragmentation across different devices by "smartly" detecting the width of the phone in its current orientation, and making the ad view that size.
Three ad heights (in dp, density-independent pixel) are available:
32 - used when the screen height of a device is less than 400
50 - used when the screen height of a device is between 400 and 720
90 - used when the screen height of a device is greater than 720
Now, get the height of AdView and adjust the margin of the layout where you wish to place the banner ad. Once the ad is loaded (by overriding on onAdLoaded API), you know the height using below method:
public static int getAdViewHeightInDP(Activity activity) {
int adHeight = 0;
int screenHeightInDP = getScreenHeightInDP(activity);
if (screenHeightInDP < 400)
adHeight = 32;
else if (screenHeightInDP <= 720)
adHeight = 50;
else
adHeight = 90;
return adHeight;
}
public static int getScreenHeightInDP(Activity activity) {
DisplayMetrics displayMetrics = ((Context) activity).getResources().getDisplayMetrics();
float screenHeightInDP = displayMetrics.heightPixels / displayMetrics.density;
return Math.round(screenHeightInDP);
}
I had the same need to be able to display my own ads when AdMob fails to receive an ad or receives an empty ad (height=0).
I use the following code based on the fact that an AdView extends RelativeLayout:
mAdMobView = new AdView(pActivity, AdSize.SMART_BANNER, Constants.ADMOB_AD_UNIT_ID);
mAdMobView.addOnLayoutChangeListener(new OnLayoutChangeListener() {
#Override
public void onLayoutChange(final View pV, final int pLeft, final int pTop, final int pRight, final int pBottom, final int pOldLeft, final int pOldTop, final int pOldRight, final int pOldBottom) {
final float lAdHeight = mAdMobView.getHeight();
if (lAdHeight == 0) {
Debug.i(LOG_TAG, "mAdMobView.onLayoutChange(...) mAdMobView.height='" + lAdHeight + "'. AdMob returned an empty ad !");
// Show custom ads
} else {
Debug.d(LOG_TAG, "mAdMobView.onLayoutChange(...) mAdMobView.height='" + lAdHeight + "'");
// Make AdView visible
}
}
});
mAdMobView.setAdListener(new AdListener() {
#Override public void onReceiveAd(final Ad pAd) {
Debug.d(LOG_TAG, "onReceiveAd(...) AdMob ad received (mAdMobView.visibility='" + mAdMobView.getVisibility() + "').");
}
#Override public void onPresentScreen(final Ad pAd) {
Debug.d(LOG_TAG, "onPresentScreen(...)");
}
#Override public void onLeaveApplication(final Ad pAd) {
Debug.d(LOG_TAG, "onLeaveApplication(...)");
}
#Override public void onDismissScreen(final Ad pAd) {
Debug.d(LOG_TAG, "onDismissScreen(...)");
}
#Override
public void onFailedToReceiveAd(final Ad pAd, final ErrorCode pErrorCode) {
Debug.i(LOG_TAG, "onFailedToReceiveAd(...) AdMob ad error (" + pErrorCode + ").");
// Show custom ads
}
});
The code in 'onLayoutChange' is executed every time Admob receives a new ad.
EDIT: My answer is not proper since this method was added with the API 11... I changed it for the use of onPreDraw() as explained in the previous answer.

View's getWidth() and getHeight() returns 0

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

Categories

Resources