Changing RecyclerView item's width makes the view disappear - android

I have the following code:
(setViewHolder is called in Adapter's OnCreateViewHolder)
override fun setViewHolder(parent: ViewGroup): BaseViewHolder<Task> {
return if (isHorizontal) {
val view =
LayoutInflater.from(parent.context)
.inflate(R.layout.horizontal_recycler_item, parent, false)
// Overriding default width
val params = view.layoutParams as ViewGroup.MarginLayoutParams
params.width = ((parent.width / 2.15) - params.marginStart - params.marginEnd).toInt()
view.layoutParams = params
TaskViewHolder(view)
} else {
val view =
LayoutInflater.from(parent.context)
.inflate(R.layout.details_list_item, parent, false)
TaskViewHolder(view)
}
}
And everything works fine until I refresh the fragment. And that's when the RecyclerView just becomes invisible. However, removing the line, where I set the width, from setViewHolder solves my issue.
What causes this?

The reason it becomes invisible is because you set width with parent.width directly,
We can not get the true value from parent.width when it is not measured.
Here provides two ways you can achieve for your requirement.
1. Calculate a View's dimension in layout after measure
view.measure(View.MeasureSpec, View.MeasureSpec)
After view.measure, we can call view.measuredWidth or view.measuredHeight to get the true value and calculate the size you want.
(view.measuredWidth / 3).toInt()
You just only need to find out what MeasureSpec you need to calculate.
But note that measure will caused a heavy-weight in loading, you need to use it carefully.
2. Set value by the screen size
We can get screen resolution in DisplayMetric by context.resources.displayMetric, so you can calculate the size you want
(context.resources.displayMetric.width / 3).toInt()

Related

Android: Set popup window width same as achor view width

I am using constraint layout in which I have arranged views using flow. These views are added dynamically.
On clicking any of view a popup window should open like shown in the illustration below:
I have managed to show the popup window but it is not taking complete width of anchor view. This is how it is looking right now:
This is the snippet from my code which shows the popup window
anchorView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
val width = anchorView.measuredWidth
val height = ViewGroup.LayoutParams.WRAP_CONTENT
val focusable = true
val popupWindow = PopupWindow(popupCardBinding.root, width, height, focusable)
popupWindow.showAsDropDown(anchorView)
How can I make it's width same as anchorView's width?
I'm not sure where you're calling measure for the anchor view, but try measuring after the view has been drawn - using UNSPECIFIED doesn't always return the correct values, if you use measure too early:
anchorView.post {
anchorView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
val width = anchorView.measuredWidth
val height = ViewGroup.LayoutParams.WRAP_CONTENT
val focusable = true
val popupWindow = PopupWindow(popupCardBinding.root, width, height, focusable)
popupWindow.showAsDropDown(anchorView)
}
If it doesn't work, try showing more code about how and when anchorView is created
I had the exact same problem and couldn't find a good answer after searching for a while.
I ended up passing the Anchor View to the PopupWindow's constructor.
public TestPopupWindow(Context context, View anchorView) {
super(context);
this.context = context;
this.anchorView = anchorView;
createView();
}
And setting the width during the view inflation.
private void createView() {
View view = View.inflate(context, R.layout.popup_window_layout, null);
this.setWidth(anchorView.getWidth());
setContentView(view);
}
Simple and works for me

Set vertical RecyclerView height to correspond to only N items height

Each row in my RecyclerView is a single-row TextView with a certain size.
I'm looking for a general implementation such that the RecyclerView's height is set to correspond to the height of N such rows.
At the moment I'm using a magic value corresponding to 3 * rowHeight but it wouldn't be valid if I would change the text size of TextView.
Is this possible?
Edit: all rows have the same height
Make your itemView a LinearLayout which have vertical orientation. Then give it's width and height programmatically in your viewHolder like this.You have to calculate each rows width and height.
LayoutParams params = layout.getLayoutParams();
params.height = your_height;
params.width = your_width;
layout.setLayoutParams(params);
However if you have only 2,3 or 4 different heights, I mean these are specific for your recyclerView, you have to use getItemViewType() and make your different layouts and different ViewHolders for each type.
I ended up using the solution given in the comment.
Inflating the row view and then changing the layout params.
fun calculateRowHeight(layoutInflater : LayoutInflater, large: Boolean = true): Float {
val textView = layoutInflater
.inflate(R.layout.recycler_view_row, null) as TextView
val fm = textView.paint.fontMetrics
return fm.descent - fm.ascent
}
recyclerView.apply {
...
layoutParams.height = N * rowHeight.toInt()
}

How to set recycler height to highest item in recyclerView?

I need to make sure that horizontal recyclerView height is the same as the height of the biggest item.
Items can have a different height (Item = always the same image + title + subtitle, title and subtitle could have infinite length).
When I set wrap_content for my recyclerView it would resize, basing on the height of visible items which makes content below recyclerView jump, and that's something I want to avoid.
What I want to achieve:
The gray area is visible viewport.
So basically I would like to get somehow hight of the biggest item, then put recyclerView height to that number.
What I already tried is approximation high of items based on length of title + subtitle but it's very inaccurate because for example even if two titles have the same text length they could have different width because of font that I use which is not a monospace font.
I just had this issue as well. My solution is:
Wrap the RecyclerView inside a ConstraintLayout.
Set the ConstraintLayout's layout_height to wrap_content.
Add an item view to the ConstraintLayout and populate it with the data of the item you expect to be the highest based on the length of its title for example.
Set the item view's visibility to invisible.
Set the RecyclerView's layout_height to zero, and make its top and bottom constraints match that of the item view.
Too late for an answer, but maybe this will help someone.
I struggled with the same issue and couldn't find an acceptable solution.
Solved by following:
First, you need to override onMeasure from the RecyclerView to save the largest element height:
class CustomRecycleView(ctx: Context, attrs: AttributeSet) : RecyclerView(ctx, attrs) {
private var biggestHeight: Int = 0
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
for (i in 0 until childCount) {
val child = getChildAt(i)
child.measure(widthSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED))
val h = child.measuredHeight
if (h > biggestHeight) biggestHeight = h
}
super.onMeasure(widthSpec, MeasureSpec.makeMeasureSpec(biggestHeight, MeasureSpec.EXACTLY))
}
}
In you layout replace RecycleView with this CustomRecycleView:
onMeasure is called when a new element in the list is visible, and if the element is the highest, then we save this value. For example: if the first element has lowest height but lates has highest then at start RecycleView will be have height match to first element but after scrolling it will stay match to highest.
If you don't need to make RecycleView height match to highest item at start then you can stop here.
To do this at the beginning, you must make a hack (based on #MidasLefko suggestion):
To find out initially what the height of the highest element will be, you need to add a scroll mechanism to the end and the beginning. I did it as follows:
private fun initRecycleView(items: ArrayList<Object>) {
val adapter = Adapter()
rv.visibility = View.INVISIBLE
rv.vadapter = adapter
rv.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
rv.setHasFixedSize(true)
rv.smoothScrollToPosition(pinnedPosts.size)
Handler().postDelayed({
rv.smoothScrollToPosition(0)
}, 300)
Handler().postDelayed({
rv.visibility = View.VISIBLE
}, 700)
}
Set the visibility of Recycle view to INVISIBLE and after 700 milliseconds to VISIBLE to make this process invisible for user. Also, scrolling to start is performed with a delay of 300 milliseconds, because without some delay it can work incorrectly. In my case, this is needed for a list of 3 elements, and these delays is optimal for me.
Also remember to remove all Handler callbacks in onStop ()
I don't think that this is possible out of the box.
Let's think for a minute about how a RecyclerView works. In order to save memory it reuses the same View objects and just binds them to new data from the list as the user scrolls. So, for example, if the user sees item's 0 and 1 then the system has only measured and laid out 2 items (and perhaps one or two more to help scroll performance).
But let's say that your tall item is number 50 in the list, when the RecyclerView binds the first few items it has no idea at all that item 50 even exists, let alone how tall it will be.
However, you can do something a bit hacky. For example, you can measure each items height after it is bound, keep track of the tallest, and then manually set the RecyclerView height to that size. With that mechanism in place you can make the RecyclerView be hidden, then manually scroll to the end of the list, scroll back to the beginning of the list, then show the RecyclerView.
Not the most elegant solution, but it should work.
Created a method to calculate the projected height of textView by trying all the description in the list to get the highest height.
public static int getHeightOfLargestDescription(final Context context, final CharSequence text, TextView textView) {
final WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
final Point displaySize = new Point();
wm.getDefaultDisplay().getSize(displaySize);
final int deviceWidth = displaySize.x;
textView.setTypeface(Typeface.DEFAULT);
textView.setText(text, TextView.BufferType.SPANNABLE);
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(deviceWidth, View.MeasureSpec.AT_MOST);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
textView.measure(widthMeasureSpec, heightMeasureSpec);
return textView.getMeasuredHeight();
}
then used this method to in onCreateViewHolder to get ready with the highest height to be used while binding the view.
MyViewHolder myViewHolder = new MyViewHolder(itemView);
for (Model m : modelList) {
currentItemHeight = getHeightOfLargestDescription(context, m.description, myViewHolder.description);
if (currentItemHeight > highestHeight) {
highestHeight = currentItemHeight;
}
}
Then used this highestHeight in onBindViewHolder` to set the height of the description TexView, so that all the views always have the same height that is equal to the highest height.
viewHolder.description.setHeight(highestHeight);
Code is committed in the
https://github.com/dk19121991/HorizontalRecyclerWithDynamicHeight
Let me know if this solves your problem, if you have some more question feel free to ask.
Thanks
To view a full discussion on this solution please see below
https://stackoverflow.com/a/67403898/4828650
You may try this:
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(#NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
final int newHeight = recyclerView.getMeasuredHeight();
if (0 != newHeight && minHeight < newHeight) {
// keep track the height and prevent recycler view optimizing by resizing
minHeight = newHeight;
recyclerView.setMinimumHeight(minHeight);
}
}
});
you should try with different item_view type
Try this
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = mLayoutInflater.inflate(R.layout.view_item, parent, false);
// work here if you need to control height of your items
// keep in mind that parent is RecyclerView in this case
int height = parent.getMeasuredHeight() / 4;
itemView.setMinimumHeight(height);
return new ItemViewHolder(itemView);
}
Or you can try this also
#Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View itemView = inflater.inflate(R.layout.itemview, parent, false);
ViewGroup.LayoutParams layoutParams = itemView.getLayoutParams();
layoutParams.height = (int) (parent.getHeight() * 0.3);
itemView.setLayoutParams(layoutParams);
return new MyViewHolder(itemView);
}
You can also set your itemView with fixed height.
I disabled the recycling in recycler view and it solved the issue.
recyclerView.getRecycledViewPool().setMaxRecycledViews(TYPE_CAROUSEL, 0);
this solution may have a performance issue if there are a lot of items but will work fine for a few items lets say 5 to 20 which was case for me.
recyclerViewHorizontal.setMinimumHeight(maxItemHeight) has worked well for me.

Android get height of a wrap content view inflated as 0dp

I'm currently working on animation that will grow up the view if the user clicks it. Basically, its a card that, when clicked, it will reveal the bottom content. For that, I'm extending Animation like this:
Val collapseAnimation = object : Animation() {
override fun applyTransformation(interpolatedTime: Float, t: Transformation?) {
val interpolatedInverted = 1 - interpolatedTime
val headerLp = headerImage.layoutParams
headerLp.width = ...
headerImage.layoutParams = headerLp
}
}
The problem is that i need to get the height of a view (wrap_content) that is defined in XML as 0dp. Basically, I want to grow up a view from 0dp to wrap_content and for that i need to know what is the wrap_content size.
How can I accomplish that in the most efficient way, without hard coding the view size?
In order to measure a view with different layout params and get its height, we can do the following:
contentContainer.measure(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
val contentContainerFinalHeight = contentContainer.measuredHeight

How to get the height of recyclerview item in "onBindViewHolder"

The height of RecyclerView item is not fixed,i need to set the background image for every item,so I want to get the height of recyclerview's item to resize the Image,but the itemView.getHeight() always return 0 in onBindViewHolder.
I have try to search many questions or articles,but i still cant get a good soluation.
Short
Measure the View manually
view.measure(
View.MeasureSpec.makeMeasureSpec(recyclerViewWidth, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
and get the height with view.getMeasuredHeight()
Background
A view will return a valid value for View.getHeight()only after it has been measured. The measuring itself will automatically happen by the system when the view is about to be displayed on screen.
When Android wants to display the layout, it will recursively call the view.layout() function for each view in the view tree. Each Parent tells its children the constraints they might have (width/height) and ask them to view.measure() themselves. As a result, the view will store the measured values BASED on the constraints in designated members (MeasuredHeight/Width). Note that at this point view.getMeasuredHeight() will hold the value while view.getHeight() will still be invalid. view.getHeight() will only return a valid value once the view has an actual height in the UI hierarchy.
Recap
So, to know the height of a view element, before it has been measured and laid out by the system, we will need to invoke the view.measure() function manually.
The measure function expects 2 parameters which derived from the view LayoutParams + the parent constraints.
In the above code sample, we are measuring the view forcing its width to be EXACTLY the width of the parent (the RecycleView), and the height is not limited.
I suggest that you define multiple layout files with the expected heights and inflate them according to some criteria in your data set.
ViewHolder onCreateViewHolder (ViewGroup parent, int viewType){
if(some condition){
//inflate layout 1
}else{
//inflate layout 2
}
or as answered here: you can get the measurements while initializing the view holder
itemView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
int width = itemView.getMeasuredWidth();
int height = itemView.getMeasuredHeight();
How about this:
view.post(() -> {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
using this code to get recycler view's item height:
view.getViewTreeObserver().addOnGlobalLayoutListener(new
ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
//don't forget remove this listener
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
//get item height here
int itemHeight = v.getHeight();
}
});
MyViewHolder.kt
With the addOnGlobalLayoutListener() method, the height value is obtained before the TextView is drawn. And then save it in a member variable.
The key is to modify the UI inside and outside the implementation of the listener so that there are no rendering problems (when the views are redrawn).
That is, you shoud use the getter inside the listener and; the setter inside and outside the listener.
companion object {
var maxHeight: Int = 0
fun create(mContext: Context): MyViewHolder {
val view = LayoutInflater.from(mContext).inflate(R.layout.item_answer, null)
updateLayout(view)
view.viewTreeObserver.addOnGlobalLayoutListener {
if (view.myTextView.height > maxHeight)
maxHeight = view.myTextView.height
updateLayout(view)
}
return MyViewHolder(mContext, view).apply {
setIsRecyclable(false)
}
}
fun updateLayout(view: View) {
if (maxHeight != 0 && view.myTextView.height != maxHeight)
view.myTextView.height = maxHeight
}
}
Source

Categories

Resources