I've implemented OnScrollListener to understand if the user is at the end of the list and I've managed to successfully appended new data to my list as follows:
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (scrollState == SCROLL_STATE_IDLE) {
if (list.getLastVisiblePosition() >= list.getCount()-1) {
currentPage++;
loadMore(currentPage);
adapter.notifyDataSetChanged();
}
}
}
The problem is notifyDataSetChanged refreshes the whole ListView as expected. But since in my list there are images that are asynchronously loaded from the internet, this refresh causes the images to load again and this is obviously not desired. The applications like Facebook, Twitter etc they seamlessly attach new data at the end without this kind of refreshing at all. How do they do it, how can I also achieve this seamless endless scroll?
I think I have to do something in my adapter's getView method to prevent reload but I could not figure it out yet. Is there a known way for it?
Thanks!
You are going to want to implement an image cache. The cache should be asked for the images and either load them asynchronously from the internet or load them from memory if used before. The cache should be used in getView.
The simplest way to do a cache is to have a hashmap (probably a ConcurrentHashMap if you are getting the images async on a separate thread), where the keys are the url and the value is the image you want to show. You can then check if the hashmap has the value first, otherwise get the image and insert in the hashmap as well. There are many other ways to do this if you look around, but this is probably the easiest.
This solution is more robust, as you do not need to worry about what to invalidate and you do not need to worry about making more than one call when requesting the same image.
Related
I'm making an API call getData(forPage: Int): Response which returns a page-worth of data (10 items max) and thereIsMoreData: Boolean.
The recyclerView is implemented that by scrolling, the scroll listener automatically fetches more data using that API call:
val scrollListener = object : MyScrollListener() {
override fun loadMoreItems() {
apiFunctionForLoading(currentPage + 1)
}
}
The problem is that with longer screen devices that have more space for items (let's say 20), the RV receives 10 items and then doesn't allow scrolling, because there's no more items to scroll to. Without scrolling, more data cannot be loaded.
My naive solution:
load first set of data
if thereIsMoreData == true I load another page of data
now I have more data than the screen can display at once hence allowing scroll
Is there a more ellegant solution?
Android has this Paging Library now which is about displaying chunks of data and fetching more when needed. I haven't used it and it looks like it might be a bit of work, but maybe it's worth a look?
Codepath has a tutorial on using it and I think their stuff is pretty good and easy to follow, so maybe check that out too. They also have this older tutorial that's closer to what you're doing (handling it yourself) so there's that too.
I guess in general, you'd want your adapter to return an "infinite" number for getItemCount() (like Integer.MAX_VALUE). And then in your onBindViewHolder(holder, position) method you'd either set the item at position, or if you don't have that item yet you load in the next page until you get it.
That way your initial page will always have the right amount of content, because it will be full of ViewHolders that have asked for data - if there's more than 10, then item 11 will have triggered the API call. But actually handling the callback and all the updating is the tricky part! If you have that working already then great, but it's what the Paging library was built to take care of for you (or at least make it easier!)
An elegant way would be to check whether the view can actually scroll down:
recyclerView.canScrollVertically(1)
1 means downwards -> returns true if it is possible tro scroll down.
So if it returns false, your page is not fully filled yet.
I am using Recyclerview to load contacts in my android app. Each row has a contact number Textview and a username Textview. Contacts load from local database but usernames should load from remote server. I have tried loading usernames for each contact in onBindViewHolder method but its get stuck whenever I scroll fast.
#Override
public void onBindViewHolder(final ContactsAdapter.ContactsViewHolder holder, final int position) {
final ContactInfo current = cDataset.get(position);
//Here I load from remote server...
}
So I want to load usernames in advance while scrolling like Endless recyclerview. Is it possible to implement this?
I am not sure I got exactly what you are saying, but it seems like you want to load all data before loading it to your recyclerView.
instead of loading from remote server on onBindViewHolder, you can load it in advance either in your Activity or Fragment and pass it to your adapter...
Don't put the get call in your onBindViewHolder, you should already have your data by now.
Either obtain your data via a call prior to initializing your Adapter or if you still need to get the data from inside the Adapter, put that call within a background thread, though you are likely to run into problems there as well if you scroll fast.
Let's say that you load items in chunks of size of 30. Add OnScrollListener on your RecyclerView. Every time you pass some threshold, for example, 20 out of 30 items, you initiate fetching the next chunk of 30 items, and, once you have them, put them at the and of the RecyclerView.
Play with this numbers. Instead of 30 and 20, you may try 100 and 50, or 50 and 10. Find numbers for which you have the best UX. However, do not count that user will always have prepared next set of data, so keep one circular ProgressBar at the end of RecyclerView as long as you have more data to load.
Another approach which you may try is to periodically start a service that will fetch all required data, prepare everything the way it is required for UI, and cache it in memory or in DB.
I am building a gallery using a ViewPager. Every time a photo is downloaded I immediately cache it in mMemoryCache which is
mMemoryCache = new LruCache<Integer, Bitmap>(cacheSize) {
#Override
protected int sizeOf(Integer key, Bitmap bitmap) {
// The cache size will be measured in bytes rather than number
// of items.
return (bitmap.getRowBytes() * bitmap.getHeight());
}
};
As you can see sizeOf() returns the number if bytes used, which makes sense. My problem is that my ViewPager's adapter needs to know how many pages I have, which would be the number of objects in my cache.
Any ideas on how I can do it? thanks!
I think you should rethink your approach. Usually, when you use some kind of paging (i mean data paging, not the ViewPager), you first add a certain fixed number of items to your Adapter. You can either start downloading the required resources right away, or wait until you really need them. Check in Adapter.onCreateView() if the resource is already downloaded and cached. If yes, get it from there, if not, start an asynchronous download and add the image to the view as soon as you got it. Show a placeholder in the meantime.
When you reach the last item in your adapter, add another batch to it and everything starts over again.
If you really want to know what items are in your LRUCache, override put() and entryRemoved(). There you know what items get added or evicted from the cache. I would advise against this method though.
I have a ListActivity that launches a task to hit a web service and display the results in a ListView. Each one of the results has an image ID attached to it.
I wrote a method that will get the image IDs of the rows displayed on screen (firstVisiblePosition() to lastVisiblePosition()) and launch a task to query another web service to get the images to display for those items. I call this method when the list's scroll state becomes SCROLL_STATE_IDLE. This makes it so the user can scroll and the task to get the images for the visible rows does not execute until the scrolling stops, preventing it from looking up images for off-screen rows.
My issue is that when the results initially show in the ListView, I can't find a good way to call my method to look up which image IDs to query for. Apparently, calling this method right after calling setAdapter does not work (I'm guessing because some of the ListView's work happens asynchronously). I am using multiple Adapter's (for reasons not pertinent to this post), so I need a good way of waiting for the list items to show before I call my method to get the IDs.
Any ideas?
After you've set the adapter or called notifyDatasetChanged() on the adapter, add your "load images" code to the list's post queue as a Runnable:
list.post( new Runnable() {
#Override
public void run() {
//do stuff
}
});
If I'm understanding your question right, you're having trouble loading images over the net and performance issues; if so,
I would create a simple image cache in my Adapter such as a local but global HashMap:
private HashMap<String, Drawable> imgCache = new HashMap<String, Drawable>();
then in my getView() method, I would asynchronously (using a Thread and a Handler) load the images and save loaded images in my imgCache by assigning position as the key and loaded images as Drawables.
final Handler h = new Handler() {
public void handleMessage(Message msg) {
if(msg.obj != null) {
Drawable drawable = (Drawable)msg.obj;
image.setImageDrawable(drawable);
image.postInvalidate();
image.requestLayout();
imgCache.put(cacheKey, drawable);
}
}
};
loadImage(myImageView, imageURL, h); // threaded method which loads the images from net
also, in my getView() method I would first ask imgCache to see if the image already exist before loadImage is called.
This should optimize your list and rescue you from using multiple Adapter etc.
Hope this helps,
-serkan
I implemented the lazy-loading images in my ListView.
I use a AsyncTask to download the image from the internet and bind it to the ImageView in the UIThread.
It's working except that when I scroll the ListView vary fast, the downloaded images sometimes are binded into the wrong items in the list.
I guess the problem is from the reuse of convertView in the BaseAdapter.
Any ideas to solve it?
Many thanks.
EDIT:
I post the answer as following:
public void setBitmap(int position, Bitmap image) {
View itemView = mListView.getChildAt(position - mListView.getFirstVisiblePosition());
if (itemView != null) {
ImageView itemImageView = (ImageView) itemView.findViewById(R.id.item_imageview);
itemImageView.setImageBitmap(image);
}
}
There are two problems that will arise during lazy loading of images in a ListView.
The old images are still shown until the new ones are loaded. This is easy just set the ImageView to an image is loading view or set it to invisible before starting the image download.
The second problem is harder to solve. Imagine you are scrolling very fast through your list. Now your views may be recycled before the old AsyncTask has finished loading the image. You now have two tasks running that in the onPostExecute method will set an image to the imageview. Now for a short time the wrong image will be shown until the second Task finishes, or even worse for some network related reason they don't finish in the order they started and you have the wrong image overwrite the correct image. To solve this you have to check what image should be displayed after the task finished. In the View class are two methods for things exact like this one:
setTag and getTag You can bind any object to the imageview that comes into your mind. In most of the cases I use setTag to bind the URL of the image as a String to the imageview before I start a task. Now I can cast getTag to a String after the task finished and compare the URL that should be displayed with the URL that I downloaded and only set the image if necessary.
Create a function called void setBitmap(Bitmap bitmap, int position) or similar in your adapter. Let your AsyncTask call this method when a new bitmap is available. This method may then call notifyDataSetChanged() on the UI-Thread itself to ensure the views get refreshed. Holding references to views in an adapter (even by holding them in an AsyncTask) is dangerous!