I have a ListView that loads images to the ImageView asynchronously. To achieve this i am using Android-Universal-Image-Loader
But i would like to start loading this images only when they are visible in the listview. For example if the visible items of the listview are from 5 to 9, only those should be loaded. Also if the user scrolls very fast the ListView only when stopped those items should be loaded.
What is the best way to this?
If you use "view reusing" in your listview adapter then you shouldn't do anything. UIL do it for you. UIL won't load ALL scrolled images, only those which are got in task pool (you can set set pool size in configuration). If you use "view reusing" then images which were scrolled fast won't be loaded.
Look into example project on GitHub.
UPD: Since 1.7.0 version UIL have PauseOnScrollListener.
boolean pauseOnScroll = true;
boolean pauseOnFling = true;
listView.setOnScrollListener(new PauseOnScrollListener(pauseOnScroll, pauseOnFling));
Use an OnScrollListener, the onScrollStateChanged() method is called when the ListView switches between SCROLL_STATE_IDLE, SCROLL_STATE_TOUCH_SCROLL (slower scrolling), and SCROLL_STATE_FLING (faster scrolling). With this you can choose to load new images only when the states is "Idle" or "Touch".
Addition
In the first run the visible items of the ListView aren't shown. For example when the app starts if the ListView has 4 items visible those 4 images should be loaded.
I haven't used the Universal Image Loader myself, but from what you described below you need to know how many rows will be displayed before you start downloading. Try this:
Write a Runnable to start the asynchronous downloads.
Use your ListView's built-in Handler to call the Runnable after the rows have been drawn
For example:
private Runnable loadImages = new Runnable() {
#Override
public void run() {
// Start the asynchronous downloads
}
};
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
mListView.post(loadImages);
}
The loadImages will be called after the ListView is drawn so you will know exactly how many rows are visible.
Related
So I have been struggling with the best way to load images in a ListView in Android for a while.
The images come from a server, so can take some time to load. From what I understand there are only 2 ways to implement this:
1 - Load the images on the main thread - This leads to have the correct images display immediately when the view displays and scrolls, but gives poor scrolling performance and can lead to the app hanging and crashing.
2 - Load the images in an AsyncTask. This means the images will be blank when the list display or scroll, but eventually display. Because of caching done by the list, you can also get the wrong image for an item. But this gives good performance and scrolling, and does not hang/crash.
Seems like neither solution works correctly. There must be a solution that works?? I have seen other posts like this, but the answer seems to always be 1 or 2, but neither is a good solution...
My code for the list adapter is below.
The HttpGetImageAction.fetchImage() method either executes an async task, or fetches the image on the main thread depending on a flag I set. I also cache the images locally and on disk once they have been loaded, so the issue mainly occur the first time (but the cache is only for 100 images, and the list has 1000s)
public class ImageListAdapter extends ArrayAdapter<WebMediumConfig> {
Activity activity;
public ImageListAdapter(Activity activity, int resourceId, List<WebMediumConfig> items) {
super(activity, resourceId, items);
this.activity = activity;
}
class ImageListViewHolder {
ImageView imageView;
TextView nameView;
TextView descriptionView;
TextView statView;
}
public View getView(int position, View convertView, ViewGroup parent) {
ImageListViewHolder holder = null;
WebMediumConfig config = getItem(position);
LayoutInflater mInflater = (LayoutInflater)this.activity.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
if (convertView == null) {
convertView = mInflater.inflate(R.layout.image_list, null);
holder = new ImageListViewHolder();
holder.imageView = (ImageView)convertView.findViewById(R.id.imageView);
holder.nameView = (TextView) convertView.findViewById(R.id.nameView);
holder.descriptionView = (TextView)convertView.findViewById(R.id.descriptionView);
holder.statView = (TextView)convertView.findViewById(R.id.statView);
convertView.setTag(holder);
} else {
holder = (ImageListViewHolder) convertView.getTag();
}
holder.nameView.setText(Utils.stripTags(config.name));
holder.descriptionView.setText(Utils.stripTags(config.description));
holder.statView.setText(config.stats());
if (MainActivity.showImages) {
HttpGetImageAction.fetchImage(this.activity, config.avatar, holder.imageView);
} else {
holder.imageView.setVisibility(View.GONE);
}
return convertView;
}
}
You should never load images on the main thread. Any network calls should be done asynchronously and should not be done on the main thread. Your images might load quickly (on the main thread) because you may have a good internet connection, have small images and/or have few images. But imagine what would happen if someone using your app has a slower internet and then one day your list grows to hundreds!
The approach below is a well accepted practice for loading scrollview content
a) load your text content as you load the scrollview. Again the loading should be done async but you can show a loading view until the download completes
b) Show image
placeholders while the image loads on each cell
c) Load the images
asynchronously
Using a library such as Picasso would help immensely because there is a lot of boilerplate code which you'd need to handle otherwise such as
cancelling image download when a cell is reused and
starting a new download for a reused cell
caching
retrying a failed download
Hope this helps.
Use Glide [https://github.com/bumptech/glide] or Picasso [http://square.github.io/picasso/] - both libraries are very similar, and easy to use, have caching, work in background, allow placeholders ets.
Usage is as simple as:
Picasso.get().load("http://i.imgur.com/DvpvklR.png").into(imageView);
Simple Solution: Glide.
Glide is a fast and efficient open source media management and image
loading framework for Android that wraps media decoding, memory and
disk caching, and resource pooling into a simple and easy to use
interface. Glide supports fetching, decoding, and displaying video
stills, images, and animated GIFs.
You can load the image like:
GlideApp.with(context)
.load("http://via.placeholder.com/300.png")
.into(imageView);
Refer for more info:
https://guides.codepath.com/android/Displaying-Images-with-the-Glide-Library
https://github.com/bumptech/glide
Use Glide library.. Its recommended by Google.
Step 1:
Use latest Glide Version 4 dependency (https://github.com/bumptech/glide)
implementation 'com.github.bumptech.glide:glide:4.8.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.8.0'
Step 2:
Glide.with(MainActivity.this)
.load(url)
.apply(new RequestOptions().placeholder(R.drawable.placeholder).error(R.drawable.error_image))//this line optional - you can skip this line
.into(imageview);
use picasso or Glide Library
What these library do , is that they image url from u and download them and save it in cache so that whenever you open the same image another time it will load faster and it prevents the usage of the network
Picasso
http://square.github.io/picasso/
Glide
https://github.com/bumptech/glide
You can try UnivarsalImageLoader which is quite Fast and Optimised.
It supports Multithread image loading (async or sync) and high level of customization.
https://github.com/nostra13/Android-Universal-Image-Loader
Here's how I do it
First I always create the RecyclerView.ViewHolder to which I pass a Func<int, Bitmap> image_provider (or you can pass some sort of lamda?) which when called will retrieve the image for a specific Id, if it can't it will show the default loading image
Everything is stored in a fragment (which I call a DataFragment) which has retainstate set to true.
First load a list of json objects that tell me which images I must show, this happens inside the data fragment so it will go on if there is a configuration change
I send that info to the adapter using a method like SetItems(MyImages)
Then I start a new thread that will load the images (while allowing the user to work)
I do this with the TPL library, the closest approximation for Android is Anko Async but you can also do it with AsyncTask, the problem is that I send a lot of messages to the main thread, so to do it with AsyncTask you have to give it a handler to the main thread which will send messages.
In the thread I loop through all the images I have to download, and download them and send a message to the DataFragment which sends it to the currently attached Activity which triggers the NotifyItemChanged method in the adapter
The adapter on creation receives a Func<int, Bitmap> image_provider which it invokes and retrieves the image from memory
I know it sounds a bit messy, so if you want code examples I can give them , but they are in C#
In your app level gradle implement below repository:
implementation 'com.squareup.picasso:picasso:2.5.2'
Put below line where you want to load image
Picasso.with(getApplicationContext()).load(config.avatar).into(imageview);
just use Picasso.get().load("http://i.imgur.com/DvpvklR.png").into(imageView); or
Glide.with(getApplicationContext()).load("image_url").asBitmap().centerCrop().into(new BitmapImageViewTarget(imageView)
in adapter like any view and done no extra efforts required
I am working on an Android app that runs on only one devicerunning KitKat.
The smooth scrolling feature for a RecylerView I used that was working on other physical tablets and genymotion has unfortunately stopped working on the one device it needs to work on.
Instead of scrolling to a certain position it passes over the target position and scrolls all the way to the bottom and looks really bad.
I am able to track down the error to the abstract SmoothScroller in the RecyclerView class.
if (getChildPosition(mTargetView) == mTargetPosition) {
onTargetFound(mTargetView, recyclerView.mState, mRecyclingAction);
mRecyclingAction.runIfNecessary(recyclerView);
stop();
} else {
Log.e(TAG, "Passed over target position while smooth scrolling.");
mTargetView = null;
}
I was using a SnappingLinearLayoutManager that I found online, but swapped it out with the normal LinearLayoutManager from Android, and still am having the same problem.
The list is 7 items long (user can see 4 at a time) and I scroll to the 5th item (position 4) item.
When I scroll to the 3rd I don't receive this error.
Also after I scroll the list up and down once, the error stops happening.
EDIT:
I am able to use layoutManager.scrollToPositionWithOffset(); But I am trying to do this with the smooth scroll animation.
Here is some of my code and details:
private void setupMainRecyclerViewWithAdapter() {
mainLayoutManager = new SnappingLinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
mainListRecyclerView.setLayoutManager(mainLayoutManager);
settingsMainListAdapter = new SettingsListAdapter(SettingsActivity.this,
settingsPresenter.getSettingsItems(),
settingsPresenter);
mainListRecyclerView.setAdapter(settingsMainListAdapter);
mainListRecyclerView.addItemDecoration(new BottomOffsetDecoration(EXTRA_VERTICAL_SCROLLING_SPACE));
}
#Override
public void scrollMainList(boolean listAtTop) {
if(listAtTop) {
mainListRecyclerView.smoothScrollToPosition(4);
moveMainMoreButtonAboveList();
} else {
mainListRecyclerView.smoothScrollToPosition(0);
moveMainMoreButtonBelowList();
}
}
If you call recyclerView.smoothScrollToPosition(pos) will be called immediately on the UI thread and if recyclerView's Adapter is too much busy to generating view items then the calling of smoothScrollToPosition will be missed then because recyclerView has no data to smooth scroll. So it's better to do that in a background thread by recyclerView.post(). By calling this it goes into the Main thread queue and gets executed after the other pending tasks are finished.
Therefore you should do something like this which worked for my case:
recyclerView.post(new Runnable() {
#Override
public void run() {
recyclerView.smoothScrollToPosition(pos);
}
});
Well, I realize it's too late, however I tried some different solutions and found one...
in custom LinearSmoothScroller I override updateActionForInterimTarget
#Override
protected void updateActionForInterimTarget(Action action) {
action.jumpTo(position);
}
It's appears not very smooth, but not instant in contrast with scrollToPositionWithOffset.
Just add one line for smooth scroll
recyclerView.setNestedScrollingEnabled(false);
it will work fine
Take a look at hasPendingAdapterUpdates(). You can use this along with a delay() for coroutines or Thread.sleep() to enable the backing data to be available before doing the scroll.
I have an android widget that displays user thumbnails in a GridView. The code follows the android sample code for implementing a widget that shows a collection. Here is the code for my getViewAt() method:
#Override
public RemoteViews getViewAt(int position)
{
if (mWidgetItems == null || position > mWidgetItems.size() - 1)
{
return getLoadingView();
}
WidgetUiUser user = mWidgetItems.get(position);
RemoteViews views = new RemoteViews(mContext.getPackageName(), R.layout.widget_grid_item);
views.setTextViewText(R.id.widgetLocation, user.getShortLocation());
views.setViewVisibility(R.id.widgetLocation, View.VISIBLE);
Intent srcIntent = ProfileActivity.createIntent(MyApplication.getInstance(), user, PageSourceHelper.Source.SOURCE_WIDGET);
srcIntent.setAction(IntentRoutingActivity.getUniqueIntentAction()); //required for this activity to work properly
views.setOnClickFillInIntent(R.id.widgetGridItemLayout, IntentRoutingActivity.createWidgetFillInIntent(srcIntent));
// Fetch bitmap synchronously
Bitmap bitmap = mImageFetcher.getBitmap(user.getImageUrl());
views.setImageViewBitmap(R.id.widgetImage, bitmap);
return views;
}
Everything works fine and all grid items get displayed correctly, providing the synchronous image load executes quickly.
However, if I run the app on a slow network where image downloads can take a few seconds, I quickly see that many of my grid items fail to load completely. I added some logcat output, and could see that the adapter fails to request the grid items that are not loading.
So for instance, my widget shows a 3x3 grid of users, and the entire GridView holds 80 or so. When working correctly, I see the adapter request around the first 20 grid items, and they all load correctly. When image loads are slow, I see the adapter request items 0, 3, 5, and 8 (for example, it varies) but no requests are made for any other items. If I start to scroll the GridView, more items start to get requested but many items still fail to request at all.
It seems that taking too much time in getViewAt() is somehow breaking the display behaviour, despite the fact that android documentation says it is fine to perform long operations in this method. Anyone seen this problem or know of a solution?
I would like to implement the same technique as used in the stock android photos app to delete pictures. What I mean by that, is that I would like to be able to select the pictures / items when perform a long item click. Then the actionbar should also display how many pictures I have selected. If you know the app, you will also know what I mean.
Basically what I have so far, is the action bar itself (I am using the appcompat one) and the gridview. There I will have add this functionlity somewhere in here:
private void setGridViewClickListener() {
mGridView.setOnItemClickListener(new OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View v, int position,
long id) {
File imgFile = new File(mImagePaths.get(position));
if(imgFile.exists()){
Bitmap myBitmap = BitmapFactory.decodeFile(imgFile.getAbsolutePath());
zoomImageFromThumb(new View(mContext), myBitmap);
}
}
});
You should set a ChoiceMode to CHOICE_MODE_MULTIPLE_MODAL to your gridview and a MultiChoiceMode listener:
gridview.setChoiceMode(GridView.CHOICE_MODE_MULTIPLE_MODAL);
gridview.setMultiChoiceModeListener(new MultiChoiceModeListener());
Then create an ActionMode (Reference) in this listener with these methods (I added a few example):
onPrepareActionMode: clear the menu, inflate the menu
onCreateActionMode: clear menu, inflate.. or whatever
onItemCheckedStateChanged: update the title in the ActionBar (like +1 item string), re/set a background to the clicked view..
onActionItemClicked: retreive the id clicked and perform actions (as notifyDataSetChanged method)
onDestroyActionMode: update the activity (like reset the background for views)
This tutorial: Multiple Selection GridView in Android might help you to do this. Also see the Guide Topic in Enabling batch contextual actions in a ListView or GridView section.
Hope this helps.
I am using volleys NetworkImageView to load imageĀ“s in an ArrayAdapter.
For some Items in my Adapter i want to use a Drawable resource instead.
Unfortunally setting the ImageResource doesnt seem to have an effect on NetworkImage.
networkImage.setImageResource(R.drawable.mydrawable); // no effect
I tryed working without NetworkImageView using an ImageLoaders get() method providing an interface. Unfortunally the view recycling didnt work out anymore and image downloads overrode already recycled and reused views.
ImageLoader.ImageListener listener =
ImageLoader.getImageListener(image, R.drawable.loading, R.drawable.error);
ImageCacheManager.getImageLoader().get(url,listener);
// OverrideĀ“s recycled views image
Any solutions?
If you only want to show drawable and not load any image at all for specific item, this is quite easy:
public void bind(Item item) {
if (item.shouldDisplayDrawable()) {
imageView.setDefaultImageResId(R.drawable.my_drawable); // show your drawable
imageView.setImageUrl(null, app.getImageLoader()); // and if url == null, Volley will not load any image to replace Default
} else {
imageView.setImageUrl(item.getImageUrl(), app.getImageLoader()); // load image from url
}
}
I tried this in my Adapter, recycling works without any issues. And you can continue using
imageView.setDefaultImageResId(R.drawable.icon_image_default);
imageView.setErrorImageResId(R.drawable.icon_image_error);
on imageView you download from url, it will display correct drawable while loading and on error.