I am working on a app that shows some places in a listview. In every listview item there is a arrow pointing towards the place(hotel, bar etc).
The problem is I don't know how to keep this arrow updated efficiently, without cashing views locally( which, according to a Google I/O video, is something i should never ever do).
The arrow needs to be updated on every phone orientation sensor event, which is many times a second.
So is there a better approach than calling notifyDataSetChanged() on every event and refiling every list item data?
UPDATE (for #dharan and anyone interested):
I have stopped working on this project because I have a full-time job now, but I thought of a solution (unimplemented/untested).
First limit the angle of rotation to a fixed step, (eg: 5, 10, 15, 20, ... 355, 360) then cache the rotated image for each angle of rotation in order to avoid expensive image rotation calculations (but at a higher memory cost).
After that I would make my getView() method know when to only update the image instead of all the data. In my case this is as easy as:
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView != null && place[position].id == (Integer)convertView.getTag()){
//the place information is already set correctly so just update the image
}
...
}
After these modifications I believe that calling notifyDataSetChanged() should not cause serious performance issues.
If you don't have too many items in the ListView you could convert to using a plain LinearLayout and control those items individually. The problem with LinearLayout though is if an item changes its size then everything (ie all children) has to be relayed out. So triggering a change in one item can re-trigger other things to layout as well. Now because you're changing a compass needle you might be able to skirt around it because that shouldn't cause each row to change its overall size. You just need to repaint that item.
The other option is to write your own layout that takes some short cuts making some assumptions the general purpose layout managers can't. The last thing I might look at is what does notifyDataSetChanged() does under the covers. You might be able to extend ListView and write a method that only redraws the row that changed (of course this assumes the height of the row hasn't changed). If the row height changes then everything after that row has to be relayed out.
Related
Short Story:
I have a (pre-caching)Custom LinearLayoutManager, RecyclerView, Custom RecyclerView.Adaptor, Custom RecyclerView.ViewHolder setup. I have only one viewType and lightweight binding functions. Its really nothing super special which is why I hoping I don't need to post code. I also don't want to confuse with all the irrelevant code.
The issue I have is that despite not failing to recycle any views, onCreateViewHolder is still being called occasionally (after initial row inflations) leaving me wondering that maybe I have a memory leak? Do you think this is the case and why? What is deciding that my app still needs to create more views instead of recycling?
I will add one thing that maybe factoring in somehow. My rows have two visual states (they expand and collapse) and it kinda appears that having a more random mix of rows in different states worsens the problem.
Full Story:
Ive noticed an occasional hiccup in the smooth scrolling of my RecyclerView. Using android studio's profiler, I have noted the following:
All my bindViewHolder methods are plenty fast and are not holding back the scrolling.
OnCreateViewHolder is what is causing the stuttering. This explains why there is always a few stutters during the first scroll. Moreover, its the inflation thats taking a ridiculously high percentage of the cpu time.
With an item/row layout constructed using ConstraintLayout, the onMeasure functions' poor performance, destroyed the scrolling performance on weaker devices.
With an item/row layout constructed using LinearLayouts, the performance improved drastically. However, the inflation of views still takes long enough to cause hickups.
With this information, I have simplified my row item's layout as much as possible making sure to use LinearLayouts. Irregardless, the rendering of the recyclerview's items shouldnt result in stuttering after the first items come on screen because one, the rows are all the same except for the data binded to them and two, the RecyclerView is supposed to recycle the rows. So onCreateViewHolder should be called a lot initially and then rarely called again. What about pre-caching? I have found this to be one reason that new viewholders are requested when scrolling. I set the cache and created a custom LinearLayoutManager that overrides the pre-caching (pre-fetching?) method called getExtraLayoutSpace(RecyclerView.State state) and adjusted the two so that there are enough existing recyclable views to cover the request during scroll. My tests confirm that after initial scroll, new views arent requested when transitioning into scrolling state.
All that and I have two issues remaining. One of them being that onCreateViewHolder is called every so often during the use of the app and this causes a little hiccup. I put a Log.w() inside onFailedToRecycleView() to see any views are not being recycled and it looks like views are being recycled. So now I think there is some memory leak and the memory profiler shows jumps in memory usage often occuring when onCreateViewHolder is called.
The three questions I posted where,
Do I have a memory leak because Recyclerview keeps creating Viewholders?
Why do I have a memory leak?
What is deciding that my app still needs to create more views instead of recycling?
I believe I have essentially found the solution to the 2 out of 3 of the questions or the respective underlying problems.
Inside Recyclerview class is a Recycler class and RecycledViewPool class. The internal Recycler object is what makes a request to the RecycledViewPool object for an available detached scrap viewholder to recycle if it doesnt already have an attached scrap viewholder to recycle. And specifically, this search for a recyclable viewholder happens inside Recycler's getViewForPosition(int position) function. If (inside this function), a recyclable view is not found, then mAdapter.createViewHolder(RecyclerView.this, type) is called. This leads to onCreateViewholder being called. So to answer the 3rd question, Recycler is deciding when to call onCreateViewholder.
Note that the RecycledViewPool's job is to maintain an array of arraylists of detached scrap viewholders (1d array of an arraylist of each viewType). The maintaining functions include clear() , reset(), and setMaxScrap(int viewType, int max) which do what their names indicate. So the pathway to the solution to second question is to create a custom RecycledViewPool that indicates when scraplist is cleared or diminished so I can track the creation and destruction of scrap views. As far as question 1, this can be solved by using setMaxScrap(int viewType, int max). Also, on initialization of the recyclerview we can put a few extra scrapviews in the pool and try to maintain a minimum number of scrapviews which can probably be done with a listener.
I am a beginner and I am having trouble with understanding a piece of code. Can someone please explain me when this function evoke and what is it for?
Here is my code :
public void onBindViewHolder(myViewHolder holder, int position) {
RecViewHolder currentdata = data.get(position);
holder.favChecker = currentdata.getFavChecker();
holder.serialID = currentdata.getSerialID();
holder.theClassName = currentdata.getTheClassName();
}
Let me start with just a little bit of background (which you may already understand, but it's needed to explain onBindViewHolder()).
RecyclerView is designed to display long lists (or grids) of items. Say you want to display 100 rows of something. A simple approach would be to just create 100 views, one for each row and lay all of them out. But that would be wasteful, because most of them would be off screen, because lets say only 10 of them fit on screen.
So RecyclerView instead creates only the 10 views that are on screen. This way you get 10x better speed and memory usage. But what happens when you start scrolling and need to start showing next views?
Again a simple approach would be to create a new view for each new row that you need to show. But this way by the time you reach the end of the list you will have created 100 views and your memory usage would be the same as in the first approach. And creating views takes time, so your scrolling most probably wouldn't be smooth.
This is why RecyclerView takes advantage of the fact that as you scroll and new rows come on screen also old rows disappear off screen. Instead of creating new view for each new row, an old view is recycled and reused by binding new data to it.
This happens exactly in onBindViewHolder(). Initially you will get new unused view holders and you have to fill them with data you want to display. But as you scroll you'll start getting view holders that were used for rows that went off screen and you have to replace old data that they held with new data.
It is called by RecyclerView to display the data at the specified position. This method is used to update the contents of the itemView to reflect the item at the given position.
for more info check RecyclerView.Adapter#onBindViewHolder
I'm going to reference the following question, because this person asked the same thing a month ago but nobody has answered him yet:
How to scroll the user at the exact position, which is somewhere between two views.?
When the user scrolls somewhere inside a recyclerview list and minimizes the app, then restores it from the bg - or goes into another activity/fragment and returns back to the recyclerview - I want the list to scroll to the exact same location the user was at. When I say the exact location I mean it has to be the exact same location - if I just tell the app to scroll to position X, then the topmost view will be at the top, but it won't be the same exact list state as the user had before. I need the list to look identical to the way it looked before the user left and returned. Is this possible, and if so, how?
My current code:
viewHolder.objectInTheList.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (recyclerViewInterface != null) {
FileAccessUtil.getInstance().setIntegerProperty("recyclerListPosition", position);
recyclerViewInterface.onRecyclerviewListEventOccured(position);
}
}
});
and then when I return to the activity I load that parameter from fileaccessutil and do:
recyclerlist.scrollToPosition(positionFromFileAccessUtil);
This solution is suboptimal because, as I mentioned before, it doesn't scroll the user to the exact identical screen position that he was at before.
Have you looked at RecyclerView.ScrollBy(x,y)?
ScrollBy(0,y) will scroll the RecyclerView in the same direction as it would if you were to move your finger up. It won't scroll to an absolute position, instead, it will just add pixels to the previous scroll position. To scroll in the opposite direction, simply use ScrollBy(0,-y). Notice that x is set to zero because you only want to scroll up/down.
Now, in order to achieve a complete solution, you will need to have a way to save the list's current scroll position. I did not test it, but I am pretty sure that RecyclerView.getScrollY() will always return 0. To overcome this, you will need to track the position yourelf by listening for scroll changes to the RecyclerView.
When you want to restore the scroll position, you can use your saved current position since the RecyclerView will, initially, have a scroll value of 0.
I've a ListView screen that is driving me mad.
Each row has three TextViews. The adapter is a custom ArrayAdapter that makes use of the view recycling mechanism: if the row is first requested, a new view is inflated, otherwise the recycled view is used. Everything works ok. I'm not showing it but for now assume it is correctly implemented.
This is how it looks normally:
There is a search button in the right upper corner of the screen. When clicked, every text view in the list is "cleared"! I've debugged the code and the textviews are there, the text is set and everything is ok except the text is barely or not shown. Here's a view dump taken with DDMS:
Looks like only the first letters of the text are shown. This only happens in certain devices, and only when the adapter's getView method returns the recycled view for the row, BUT only after pressing the search button, which makes the keyboard to show up (If i close it, the text is still missing). If I modify the getView method to always inflate every row this does not happen, but of course this is not an option since my list should be able to handle a large number of items.
Looks like a bug, but I need to do something about it.
I've also tested an alternative recycling mechanism based on a map (basically I put the view for each row in a map). Still the behavior is observed.
Device is a Samsung Galaxy SII running 4.1.2. Not happening in Galaxy SIII mini for instance.
Do you know what could be causing this glitch? Something related to the TextView's width?
Thanks in advance
I finally found the cause.
It was one of the TextViews in the adapter xml file. It was contained in a fixed-width LinearLayout. Its width was set to fill_parent, and changing it to wrap_content fixed the issue.
In the getView() method of my GridView, I am doing the following:
public View getView(int position, View convertView, ViewGroup parent) {
convertView = this.inflater.inflate(..);
ImageView image = (ImageView) convertView
.findViewById(R.id.imageButton1);
imageLoader.displayImage("URL/"
+ sampleArray.get(position), image);
sampleArray here is an array I've loaded when the Adapter is created. It is used when lazy-loading (with this tool: https://github.com/nostra13/Android-Universal-Image-Loader) these images and basically is a part of the image URL that determines the picture to load. Now, as far as I know, best practice is to do only inflate the view if it's null:
if(convertView == null){
this.inflater.inflate(...);
When I do this, it is infact faster, but the image loading is weird. If I scroll down and scroll back up, the images switch within the row, meaning in the top row, the 1st column image might randomly switch with the second column image. The array I am using (sampleArray) doesn't change. I know it has something to do with position and getView() being called, but I'm not sure why this behavior happens.
If I do it the way I am doing now (the first block of code), meaning I inflate it every time, it works perfectly but loads slowly. Why does this behavior work like this?
Thanks in advance.
EDIT:
Here's what I think is happening. GetView() is being called multiple times, which is somehow not in sync with the "position" argument, causing the wrong element to be picked out of the array. I'm still puzzled by it.
You should use
.resetViewBeforeLoading()
and probably
.showStubImage(R.drawable.empty_image)
on your DisplayImageOptions when you initialize the UIL.
This will reset the view before the loading.
This is needed because you are reusing the views on gridview.
Have you tried to clean up the original image:
image.setImageBitmap(null);
I check the source code of the image loader, one of the methods is like:
public void displayImage(String uri, ImageView imageView, DisplayImageOptions options)
I think you can also try to set the option as "resetViewBeforeLoading = true"
the reason I think that the weird thing happens is, the view is recycled by the gridview, which is still with the original bitmap when showing up, before the loader attach it with the new bitmap.
if it still doesn't work, please lemme know.
EDIT
I did more investigation, the reason I think is from the recycling.
Let me explain more clearly:
reason:
suppose we have total 12 cells in our data
on screen, there are only 9 views visible
at very beginning, the gridview will create and show 9 on screen:
view_1_1 view_1_2 view_1_3
view_2_1 view_2_2 view_2_3
view_3_1 view_3_2 view_3_3
after scrolling(scrolling up, to show the row 4), at the moment, grid view will put view_1_1, view_1_2, view_1_3 in recycle bin.
and then will get 3 views for row 4 from recycle-bin, which are put in the getView as "convertView"
so, the view_1_1, view_1_2, view_1_3 will be reused.
suppose at the beginning, the views are attached with bitmap
view_1_1 : bitmap_1_1
view_1_2 : bitmap_1_2
view_1_3 : bitmap_1_3
when these view are recycled and show at the row 4, since the bitmaps are loaded asynchronously, the bitmap_1_1, bitmap_1_2, bitmap_1_3 will show up on the row 4, and laterly be replaced by those new bitmap.
how to confirm it
1.one way for debugging grid view I like is put some "id" on the image, the simple way is just put "ImageView" toString()
put an overlay view above each imageView, and write id in it, like:
String s = viewInfoHolder.imageView.toString();
viewInfoHolder.overlay.setText(s.substring(s.length() -3));//last 3 chars are enough and cleanup
2.for making the replacement slow, modify the
com.nostra13.universalimageloader.core.LoadAndDisplayImageTask
the last row in method:
public void run()
from:
handler.post(displayBitmapTask);
to:
handler.postDelayed(displayBitmapTask, 2000);
and you could find out exactly how the grid view works.
Hope it helpful :)