I am trying to improve our list view rendering performance and looking into the fine tuning now. (we use viewHolder, fetch images async, pause image displaying on scroll, disabled scrolling cache already)
Now I was inspecting the layout and came across a setup like the following for the single list item's layout, which gets inflated in getView.
getView() of custom list adapter
if (convertView == null) {
convertView = inflater.inflate(R.layout.zzz_list_item, null);
...save stuff in holder etc.
zzz_list_item.xml
<LinearLayout
android:id="#+id/layout_success"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/base"
android:orientation="vertical"
android:paddingBottom="30dp" >
...many lines of a "success" item layout
</LinearLayout>
<LinearLayout
android:id="#+id/layout_failure"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="gone" >
...many lines for a failure item layout
</LinearLayout>
The visibility gets controlled further down in the getView method of our custom listview adapter
getView() method - executed every time
if(isSuccessfulItem){
((LinearLayout) convertView.findViewById(R.id.layout_failure)).setVisibility(View.GONE);
((LinearLayout) convertView.findViewById(R.id.layout_success)).setVisibility(View.VISIBLE);
} else {
((LinearLayout) convertView.findViewById(R.id.layout_failure)).setVisibility(View.VISIBLE);
((LinearLayout) convertView.findViewById(R.id.layout_success)).setVisibility(View.GONE);
}
While this also might be a small performance hog (getView is expensive), I wonder if it would make sense to refactor and split the handling for success and failure elements into two different layouts, which would then be inflated respectively using getViewTypeCount() and getItemViewType(int position).
Does the additional failure layout code (and therefore increased file size) for my list item layout affect the performance, even if it is set to visibility=GONE during inflation?
Any insights would be much appreciated, thanks.
Does the additional failure layout code (and therefore increased file
size) for my list item layout affect the performance, even if it is
set to visibility=GONE during inflation?
Either way the performance gain or loss it's minimal. A view with visibility set to gone doesn't need calculation in the layout and measuring phase as it's ignored but it does consume memory(and this is what you could talk about). With your current implementation even if you don't need/use the failure part of the row layout you do have it occupying memory (multiply this by the number of rows visible on the screen). Splitting the current row in two parts will clear that memory need as each row will have only the views it actually uses.
If I were you I would implement two row types as I think it's cleaner(and it also doesn't add useless views in the memory).
You probably know this but use:
convertView = inflater.inflate(R.layout.zzz_list_item, convertView, false);
also, cache the row views in the holder.
Related
Do I have to rely on ListView/Recycler view each time I need to loop over data to repeat a layout ?
I totally understand it for long lists of data where scroll/performance is involved, but let's say I am sure i'll only have 0...3max items and need to display in very simple single-line-layout for each (1 image, 1 textview + button).. isn't there a simplier pattern than using adapters ?
Seems like overkill (and a pain to deal with for every little part of my screen where I need to loop overs small lists).
What are the other options while using Components architecture (databinding) ?
Manually inflating my layout ? In viewmodel ? fragment? Do I need to create another viewModel specially for this child layout ?
Thanks
I recently have a similar issue recently, but my problem was that of nested lists i.e. I needed to inflate another list inside a recycler view. Here is a minimal example of how I went about it.
Add a LinearLayout to your layout XML file:
<LinearLayout
android:id="#+id/smallList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:list="#{viewModel.yourList}" />
Create a binding adapter where you inflate the items. Like so:
#BindingAdapter("app:list")
fun setList(layout: LinearLayout, yourList: List<ListItemModel>) {
layout.removeAllViews() // Remove previous items if your list does change
for (listItem in yourList) {
ListItemBinding.inflate( // inflate your list item
LayoutInflater.from(layout.context),
layout, // pass your LinearLayout as root
true // attachToRoot is true so that the inflated view is added to the LinearLayout
).apply {
// set your binding variables
this.listItem = listItem
}
}
}
Note: This is a minimal example to solve the issue since actual data and functionality is unknown. You may want to:
Add a click listener variable to your list item XML file and set that similarly.
Create a custom view for the view if it is to be reused and write the binding adapter there.
What I intend to achieve
The item view should occupy the entire height of the item
It could be that the item height is lesser than the height of the tallest item in the recyclerview, in which case it should just stick to the top like in the screenshot above.
The bug I'm running into
As in the screenshot above, views are getting truncated.
What I've tried so far
Initially I went with wrap_content on the recyclerview, now that it is supported. It didn't work when none of the views visible on the screen at the time were the tallest. This makes sense in how the view hierarchy is laid out. How can the height of something which hasn't even been bound to any data yet be calculated if the height is dependent on that data?
Workaround time :S
Instead of trying a custom layoutmanager, I first went with what I felt needed to be done - laying out all item views at the beginning to figure out their height.
There's a progressbar and an animation playing in the upper part of the screen to catch the user's attention while all this happens with recyclerview visibility set to invisible. I use two things, one didn't suffice - I've attached an observer in the adapter's onViewAttached() call and I've used a scroll change listener as well. There's a LinearSnapHelper attached to the recycler view to snap to adjacent (next or previous, depending on the scroll direction) position on scroll.
In this setup,
I'm going to each position in the recyclerview using layoutManager.smoothScrollToPosition()
Getting the child view height using
View currentChildView = binding.nextRv.getChildAt(layoutManager.findFirstCompletelyVisibleItemPosition());
if (currentChildView != null) {
currentChildHeight = currentChildView.getHeight();
}
in scroll change listener on RecyclerView.SCROLL_STATE_IDLE or by passing the height to the view attached observer mentioned above in the adapter's onViewAttachedToWindow()
#Override
public void onViewAttachedToWindow(BindingViewHolder holder) {
if (mObserver != null) {
mObserver.onViewAttached(holder.binding.getRoot().getHeight());
}
}
Storing a maxHeight that changes to the max of maxHeight and new child's height.
As is evident, this is ugly. Plus it doesn't give me the current view's height - onAttached means it's only just attached, not measured and laid out. It is the recycled view, not the view bound to current data item. Which presents problems like the truncation of view illustrated above.
I've also tried wrap_content height on the recycler view and invalidating from recycler's parent till the recycler and the child on scroll coming to SCROLL_STATE_IDLE. Doesn't work.
I'm not sure how a custom layoutmanager can help here.
Can someone guide me in the right direction?
I could not accept #Pradeep Kumar Kushwaha's answer because against one solution, I do not want different font sizes in the list. Consistency is a key element in design. Second alternative he gave couldn't work because with ellipsize I would need to give a "more" button of some sort for user to read the entire content and my text view is already taking a click action. Putting more some place else would again not be good design.
Changing the design with the simple compromise of resizing the recyclerview when the tallest, truncated item comes into focus, it turns into the simple use case of notifyItemChanged(). Even for the attempt I made using the view attached observer and scroll state listener, notifyItemChanged could be used but that approach is just too hacky. This I can live with in both code and design. Here goes the code required.
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
int position = ((LinearLayoutManager) binding.nextRv.getLayoutManager())
.findFirstVisibleItemPosition();
if (position != nextSnippetAdapter.getItemCount() - 1) {
binding.nextRv.getAdapter().notifyItemRangeChanged(position, 2);
} else {
binding.nextRv.getAdapter().notifyItemChanged(position);
}
}
}
For my particular setup, calling for just these two elements works. It can further be optimized so as to call for single element at position + 1 in most cases, and checking and calling for the appropriate one in corner (literal) cases.
Inside your adapter where I can find two cards one on top and another on bottom
How I would have defined my layout is like this:
Cardview1
LinearLayout1 --> orientation vertical
cardview2 (Top card where text is written)
Linearlayout2 (where I can see icons such as like etc)-->orientation horizontal
Now fix the height of Linearlayout2 by setting it to wrap content.
And the height of cardview2 should be 0dp and add weight = 1
Now inside cardview2 add a TextView1 to matchparent in height and width.
Better inside textview1 add ellipsize to end and add max lines
If you want to show all lines try to find autoresizetextview library it can be founded here --> AutoResizeTextView
Hope it helps.
I think the recyclerview can be set to height wrap_content. And the items can be make like height to match_parent.
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layput_height="wrap_content"/>
Item as:
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
// your coode
</androidx.constraintlayout.widget.ConstraintLayout>
I had little more requirement than the question. Even my problem solved in the way.
Remember I am using:
androidx.recyclerview:recyclerview:1.0.0-beta01
dependency for the project
I have a similar problem to the one in this question: ListView and rows recycling problem
Above is solved. but same solution does not work for me.
I have a listview consisting of textviews. Each textview can contain variable number of images. I am getting images with html.imagegetter and I am using recycling in getview method of list adapter.
If there is one or more images in a textview, it will have a height to be able to fit its content. When this textview is recycled and new content does not fill the height defined before, then empty space will appear.
The problem is that I can't restore the textview height in getview because I dont know what height the next list item will have.
this is my getview:
public View getView(int position, View convertView, ViewGroup parent) {
final ViewHolder holder;
View rowView = convertView;
if (rowView == null) {
LayoutInflater inflater = context.getLayoutInflater();
rowView = inflater.inflate(resource, parent, false);
holder = new ViewHolder();
holder.imgAvatar = (ImageView) rowView.findViewById(R.id.imgAvatar);
holder.textDetails = (TextView) rowView.findViewById(R.id.textDetails);
holder.textPostTime = (TextView) rowView.findViewById(R.id.textPostTime);
holder.textPost = (TextView) rowView.findViewById(R.id.textPost);
rowView.setTag(holder);
} else {
holder = (ViewHolder) rowView.getTag();
}
holder.textDetails.setText(details.toString());
holder.textPostTime.setText(x.postTime);
holder.textPost.setText(Html.fromHtml(x.postBody, new URLImageGetter(holder.textPost, context), null, context));
return rowView;
}
this is row layout:
<TextView
android:ellipsize="none"
android:autoLink="web"
android:textColor="#000000"
android:id="#+id/textPost"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
THE PROBLEM: The listview recycling mechanism can't calculate the new height for used views according to its new contents.
Without recycling, it is too slow.
So, essentially your problem is with recycling the views. When your views get inflated from XML they look at the images and decide how tall to be. This works fine because the inflater can cause the height to be "wrap_content" aka however tall the tallest child component is - this is dynamic.
However, when your getview recycles the old views it (for some reason) isn't updating the height of the rows, and neither are you (which you never had to do before because the inflater handles it automatically from "wrap_content"). It would appear from what you described that you just get a row with whatever the height of the view happened to be set at previously (this previous value was set by the inflater automatically).
Your two solutions are:
Don't recycle the views - just inflate a new one every time. You will see a performance hit which will probably only be noticeable at lists with ~50 elements or more. If it is a small list this may be the easiest (code-wise) option, although there it will still operate slightly slower.
Keep recycling the views - if you pick this you will have to set the height of the row (or text view, whatever is determining it) yourself based on the image heights. This is doable but will require more work. I'd suggest
secret option 3 - normalize the image heights - If you would a.) like to continue recycling views but b.) don't want to write the code to set the row heights manually and c.) resizing the images wouldn't kill your app, you could use the setMaxHeight(int) method to make sure all of your images are the same height (or use a method like this to resize the images to a predetermined size)
Note: I am pretty sure (almost certain) that, normally, if height is set to "wrap_content" it would wrap to the new content size (since it normally updates the size when the content is updated). I couldn't say why this is not happening for you - it may be that something inside the "wrap_content" has its size set to a concrete value and thus isn't resizing, so the "wrap_content" isn't changing even though there is empty space.
Try using requestLayout on your image and textviews. It will help the listview recompute the height of recycled views.
I have a row for a ListView defined as :
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="#+id/menutext"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textSize="24sp"
android:layout_alignParentLeft="true"/>
<ImageView
android:id="#+id/icon"
android:layout_width="40dip"
android:layout_height="40dip"
android:src="#drawable/lock"
android:layout_gravity="right"
android:layout_alignParentRight="true"/>
</LinearLayout>
My adapter for the ListView is set in onCreate()
listView.setAdapter(new ArrayAdapter<String>(this, R.layout.send_menu_row, R.id.menutext, items));
I have 5 rows with a text and image on each row. In onResume(), I want to make the first row's ImageView invisible.
#Override
protected void onResume()
{
super.onResume();
LinearLayout linLayout = (LinearLayout) listView.getAdapter().getView(0, null, null);
ImageView v = (ImageView) linLayout.getChildAt(1);
v.setVisibility(View.INVISIBLE);
}
But it doesn't change the visibility. Can someone help me on this?
Probably what is happening is that you are not doing that in the right method.
Try to switch the device orientation from horizontal to vertical (or vice-versa). This should trigger onResume method to be invoked and it could work.
Anyway, hiding an image shouldn't be done that way. Perhaps you should use an empty image or override the getView method (in the adapter).
UPDATE - why do I say you shoudn't use this method to do that
The thing is adapter.getView is used to get a view that will be drawn. The OS calls this method when he needs to draw that item on the screen.
This method could be overriden by the developer to draw custom/complex views but it should be used (as in, invoked) exclusively by the system.
For example, when we are talking about long lists, if you scroll, you'll have the getView method invoked and it will receive a view to be reused (which is a lot more efficient). This means that if you hard-core that the first view will be invisible, when you scroll and the first view is reused to display the 20th item (for example), now the 20th item would be invisible because probably you would just update the label and image source.
Note:
When I say first view, I'm referring to the view where the first item is initially drawn. Later, the view that was used to accommodate the first item is going to be reused to accommodate another item.
What is happening:
I think I got it now. What I believe to be happening is the following:
When the activity is initially drawn, you'll have the getView method invoked 5 times (one for each item that you are displaying). Each time the OS collects the view returned and adds it to the listview.
Latter, you'll call getView by yourself. As you pass no view to be reused, the method will create another view and return it. What is different this time? You are not adding this view to the listview. (Also, this isn't what you what to do.)
what you wanted to do is get the view that was used to draw the first item. But in this case you are just getting another view that could be used to draw the first item.
The solution is overriding getView or using a transparent image (easier).
Here's a link for the first result on google:
http://www.softwarepassion.com/android-series-custom-listview-items-and-adapters/
How about override getView and then do setVisibility when you return the first row in getView()?
It's not a great idea to be modifying rows outside of the list adapter. Since everytime the user scrolls you will lose the changes.
A workaround maybe ?
Try making a transparent image (png) and put that in as first item :-)
I have a quite complex View build-up, and as a part of that, I have a ListView inside a LinearLayout within a ScrollView (and quite a lot more components, but they do not matter in this issue).
Now the whole activity scrolls nicely as it should, but the ListView has a limited height, and when the Items inside it surpass the height, the disappear of my screen. I've tried to place the ListView inside it's own ScrollView, but this doesn't work. When I try to scroll on the ListView, the main ScrollView is selected and my screen scrolls instead of the ListView.
My question may sound easy, but I haven't been able to fix this... Is it possible to make the ListView scrollable aswell?
The relevant XML:
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout android:id="#+id/GlobalLayout"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<ListView android:id="#+id/EndpointList"
android:choiceMode="multipleChoice"
android:layout_height="175dip"
android:layout_width="fill_parent" />
</LinearLayout>
</ScrollView>
Instead of ListView with other layouts inside ScrollView create one ListView with header and footer.
Add views that should be above ListView as a header:
addHeaderView(View v)
and that below as a footer:
addFooterView(View v)
Put everything what should be above ListView to header of ListView and the same with footer.
LayoutInflater inflater = LayoutInflater.from(this);
mTop = inflater.inflate(R.layout.view_top, null);
mBottom = inflater.inflate(R.layout.view_bottom, null);
list.addHeaderView(mTop);
list.addFooterView(mBottom);
// add header and footer before setting adapter
list.setAdapter(mAdapter);
In result you'll get one scrollable view.
Actually, the way that I have it set up is really working... Placing a ListView in a LinearLayout within a ScrollView. Just avoid that the ListView is the direct child of the ScrollView, and it will work out just fine...
Just be aware that if there aren't enough items in the ListView to fill it so it goes 'off screen', that it won't scroll (kind of logically though). Also note that when you have enough items to scroll through, you need to keep pressing on an item in the ListView to make it scroll, and half of the time, focus is given to the global scrollview in stead of the ListView... To avoid this (most of the time), keep pressing on the most top or most down item, depending on which way you want to scroll.This will optimize your chance to get focus on your ListView.
I've made a video that it is possible, am uploading it now to YouTube...
Video is http://www.youtube.com/watch?v=c53oIg_3lKY. The quality is kinda bad, but it proves my point.
Just for a global overview, I used the ScrollView to be able to scroll my entire activity, the LinearLayout for the Activity's layout, and the ListView to, well, make the list...
I would like to just note something for the video
The listview is working once every x touches not because you placed it inside a linearlayout but because you are touching the the divider...
the scrollview will then consider that the place you touched does not have children to dispatch the motionevent to... so it calls the super.dispatchTouchEvent which is in this case the View.dispatchTouchView hence the listview.onTouchEvent.
When you touch inside a row the scrollview which is really a viewgroup will send the dispatch to the children in your case the textview and never calls the one of the view so the listview do not scroll.
Hope my explanation was clear enough to point out why is it not working.