Being a complete novice to Android and (admittedly) not the strongest programmer - I want to ask for some advice on loading thumbnail images into a Bitmap Array, which is loaded into a custom adapter.
The thumbnails are very small (around 5KB).
I add the thumbnails to a Bitmap array in an Async task. I am using drawables which are dummy images. So I load the entire list with dummy images (I load the actual images later on).
I am worried if the user browses a folder with 200+ images. I could possibly get an out of memory error. I want a way to prevent this, perhaps only load what is needed in the visible display, and load more if needed?
I have read a lot of other questions and advice on recycling Bitmaps, but I'm still not sure where tog o from here.
#Override
protected Boolean doInBackground(DbxFileSystem... params) {
//Opens thumbnails for each image contained in the folder
try {
DbxFileSystem fileSystem = params[0];
Bitmap image=null;
int loopCount=0; //I use this to identify where in the adapter the real image should go
for (DbxFileInfo fileInfo: fileSystem.listFolder(currentPath)) {
try{
if(!fileInfo.isFolder)
{
image = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
pix.add(image);
paths.add(fileInfo.path);
loopCount++;
}
else
{
//must be a folder if it has no thumb, so add folder icon
image = BitmapFactory.decodeResource(getResources(), R.drawable.dbfolder);
pix.add(image);
paths.add(fileInfo.path);
loopCount++;
}
}
catch(Exception e)
{
e.printStackTrace();
}
System.gc();
}
}
catch (Exception e) {
e.printStackTrace();
return false;
} finally {
loadingDialog.dismiss();
}
return true;
}
Here is the getView from the Custom Adapter:
public View getView(final int position, View arg1, ViewGroup arg2) {
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View v = arg1;
ViewHolder holder;
if (arg1 == null) {
LayoutInflater vi = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
v = vi.inflate(R.layout.list_row, null);
holder = new ViewHolder();
holder.title = (TextView) v.findViewById(R.id.filename);
holder.iconImage = (ImageView) v.findViewById(R.id.list_image);
holder.checkbox = (CheckBox)v.findViewById(R.id.checkBox1);
v.setTag(holder);
} else {
holder = (ViewHolder) v.getTag();
}
holder.title.setText(folderName.get(position).toString());
holder.iconImage.setImageBitmap(images.get(position));
The first thing you need to know is that when using an adapter, the views are created only when they are displayed on the screen. It means that you don't need and you must not decode all the bitmaps.
The best practice is to decode each bitmap in an AsyncTask when the associated view is created. The bitmap will be decoded in the doInBackground method and set to the ImageView in the onPostExecute method (as it's executed on the UI thread).
Then you might also want to use RAM or disk cache to reload previously decoded bitmaps more efficiently.
Please take a look at http://developer.android.com/training/displaying-bitmaps/index.html for more information about how to display bitmaps efficiently.
Related
I am working on social application and it's about to complete but I got stuck on one issue that is image flickering. When there is around 9 to 10 images on screen and if I scroll the page then the image flicker take place.
#Override
public View getView(final int position, View convertView, ViewGroup parent) {
final ViewHolder holder;
if (convertView == null) {
LayoutInflater inf = (LayoutInflater) act.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inf.inflate(R.layout.view_grid_explore, null);
holder = new ViewHolder();
holder.img = (ImageView) convertView.findViewById(R.id.img_grid_album);
} else {
holder = (ViewHolder) convertView.getTag();
}
ImageLoader.getInstance().displayImage(
Static_Urls.explore_pic + data.get(position).talk_pic,
holder.img);
convertView.setTag(holder);
notifyDataSetChanged();
return convertView;
}
Note : Don't forget to remove notifyDataSetChanged();.
This is happening because once the images are downloaded in the device by UIL(Universal Image Loader), it caches the images in Memory and device.
By using this code :
ImageLoader.getInstance().displayImage(Static_Urls.explore_pic +data.get(position).talk_pic,
holder.img);
every time getView() is called UIL tries to fetch the image from network, but by the time it releases that image is already being cached so it shows the image after making a network request first.
so in order to get rid of this flickering use this code :
ImageLoader imageLoader = ImageLoader.getInstance();
File file = imageLoader.getDiskCache().get(Static_Urls.explore_pic +data.get(position).talk_pic);
if (file==null) {
//Load image from network
imageLoader.displayImage(Static_Urls.explore_pic +data.get(position).talk_pic,
holder.img);
}
else {
//Load image from cache
holder.img.setImageURI(Uri.parse(file.getAbsolutePath()));
}
This code will first check whether the image is already cached or not, Then accordingly fetch image from Network or from cache.
The notifyDataSetChanged() line is redundant there. Working with adapters always keep in mind that(in case of adapters extending BaseAdapter) the getView() method is responsible for inflating the layout of the list item and also updating the UI if if you handle it so(normally you do)
Calling notifyDataSetChanged() will cause the getView() being called again right away which is why you see the flickering.
You should only call notifyDataSetChanged() when you would like to update the adapter content. One example would be when you build yourself a "refresh()" method inside your adapter like:
public void refresh(List<Object> list) {
data.clear();// Assuming data is a List<> object or an implementation of it like ArrayList();
data.addAll(list);
notifyDataSetChanged(); // This will let the adapter know that something changed in the adapter and this change should be reflected on the UI too, thus the getView() method will be called implicitly.
}
I have a listview which loads images in every cell in async. When I try to scroll down slowly(after all the images in the current view are loaded), it works flawlessly.
But when I try to scroll down before they are even loaded and scroll up, I face this issue. The cells start to show up images which don't correspond to them.
My getView method looks like this:
public View getView(final int position, View convertView, ViewGroup parent) {
// TODO Auto-generated method stub
View rowView = null;
if(convertView == null) {
rowView = inflater.inflate(R.layout.list_posts_item, null);
final Holder holder=new Holder();
holder.tvTitle=(TextView) rowView.findViewById(R.id.tvTitleNamePost);
holder.ivPrimaryImage=(ImageView) rowView.findViewById(R.id.ivPrimaryImage);
holder.tvLocality=(TextView) rowView.findViewById(R.id.tvLocalityPosts);
holder.tvDateCreated=(TextView) rowView.findViewById(R.id.tvDateCreated);
rowView.setTag(holder);
}else {
rowView=convertView;
}
Holder holder = (Holder)rowView.getTag();
holder.ivPrimaryImage.setId(position);
holder.ivPrimaryImage.setTag(listOfPosts.get(position).getPostId());
holder.ivPrimaryImage.setImageBitmap(null); // Added for flickering issue
holder.tvTitle.setText(listOfPosts.get(position).getTitle());
holder.tvLocality.setText(listOfPosts.get(position).getLocality());
holder.tvDateCreated.setText(listOfPosts.get(position).getCreatedDate());
postId = listOfPosts.get(position).getPostId();
Image image = new Image();
image.setImg(holder.ivPrimaryImage);
if (!"N".equalsIgnoreCase(listOfPosts.get(position).getHasImage()) ) {
if(!tagsCaching.containsKey(postId))
new GetPrimaryImages().execute(image);
else
holder.ivPrimaryImage.setImageBitmap(tagsCaching.get(postId));
}
return rowView;
}
And my Async call class looks like this:
public class GetPrimaryImages extends AsyncTask<Image, Void, Bitmap> {
ImageView imageView = null;
protected Bitmap doInBackground(Image... images) {
this.imageView=images[0].getImg();
// Building Parameters
List<NameValuePair> params = new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair("postid",(String)(this.imageView.getTag()) ));
json = jsonParser.makeHttpRequest(CommonResources.getURL("get_primary_image"),
"POST", params);
if(json == null){
return null;
}
Log.d("Fetching Image",imageView.getTag()+ json.toString());
tagsDownloaded.add((String)imageView.getTag());
// check for success tag
String TAG_SUCCESS = "success";
try {
int success = json.getInt(TAG_SUCCESS);
if (success == 0) {
image = json.getString("primaryimage");
}
} catch (JSONException e) {
e.printStackTrace();
}
return getImage(image);
}
/**
* After completing background task Dismiss the progress dialog
* **/
protected void onPostExecute(Bitmap result) {
tagsCaching.put((String)imageView.getTag(), result);
imageView.setImageBitmap(result);
}
public Bitmap getImage(String imageString) {
if("null".equalsIgnoreCase(imageString)){
return null;
}else{
byte[] decodedString = Base64.decode(imageString, Base64.DEFAULT);
Bitmap decodedByte = BitmapFactory.decodeByteArray(decodedString, 0, decodedString.length);
//image.setImageBitmap(decodedByte);
return decodedByte;
}
}
}
Edit:
I added a new instance variable to Holder:
public class Holder
{
TextView tvTitle;
ImageView ivPrimaryImage;
TextView tvLocality;
TextView tvDateCreated;
int position;
}
Set the same in the getView:
holder.position = position;
And passed the holder object to the Async task:
new GetPrimaryImages(position, holder).execute(image);
And modified the Async call class as follows:
1. Added cancel to the http call
2. Changed the onPostExecute method
public class GetPrimaryImages extends AsyncTask<Image, Void, Bitmap> {
int mPosition;
Holder mHolder;
public GetPrimaryImages(int position, Holder holder){
mPosition = position;
mHolder = holder;
}
ImageView imageView = null;
protected Bitmap doInBackground(Image... images) {
this.imageView=images[0].getImg();
List<NameValuePair> params = new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair("postid",(String)(this.imageView.getTag()) ));
JSONObject json;
if(mHolder.position == mPosition)
json = jsonParser.makeHttpRequest(CommonResources.getURL("get_primary_image"),
"POST", params);
else {
json = null;
cancel(true);
}
// check log cat fro response
if(json == null){
return null;
}
Log.d("Fetching Image",imageView.getTag()+ json.toString());
tagsDownloaded.add((String)imageView.getTag());
// check for success tag
String TAG_SUCCESS = "success";
try {
int success = json.getInt(TAG_SUCCESS);
if (success == 0) {
image = json.getString("primaryimage");
}
} catch (JSONException e) {
e.printStackTrace();
}
return getImage(image);
}
protected void onPostExecute(Bitmap result) {
if (mHolder.position == mPosition) {
tagsCaching.put((String) imageView.getTag(), result);
imageView.setImageBitmap(result);
}
}
public Bitmap getImage(String imageString) {
//needs to wait
if("null".equalsIgnoreCase(imageString)){
return null;
}else{
byte[] decodedString = Base64.decode(imageString, Base64.DEFAULT);
Bitmap decodedByte = BitmapFactory.decodeByteArray(decodedString, 0, decodedString.length);
//image.setImageBitmap(decodedByte);
return decodedByte;
}
}
}
It seems to be working. :)
Now my doubt is what would be the best way to cache the images? Should be writing it to a file? and reading it from it every time I scroll up?
The problem is, while your async task ends its background operation, the element it was linked to has been recycled to hold another element of your collection.
Let's focus on elements position, and let's say your listview can display up to 4 elements.
The first time the listview calls getview for the first 4 elements, and four asynctasks are created and run.
Then you scroll to shouw positions 11 - 15, and the first element (the one related to position 1) gets recycled for position 11 before the asynctask ends.
Then the asynctask ends, and what you see is the image related to post 11 with the bitmap related to post 1.
A way to avoid this is knowing in the asynctask that the view was recycled, as suggested in this old post from Lucas Rocha.
Performance tips with listview
Check the post for insights on how listview works too:
Main "problem" is with ListViews implementation of reusing views and serial providing of AsyncTasks.
1) In ListView's adapter you correctly implement reusing of items. In ListView there are rendered only few items (items visible on screen + few top and down). If you start scrolling items which went out of screen are destroyed and theirs views are passed asi parameter to public View getView(final int position, View convertView, ViewGroup parent) as convertView.
This is first problem. List is reusing items.
2) Second thing is that AsyncTask is performed on another thread but more instances of asyncTask are performed on the same thread. it means they are performed serially. If you scroll quickly enough then you can make more requests on loading images with AsyncTask. At one time, there can be only few (i think 5) AsyncTask's jobs waiting in queue. Another ariving is ignored. So if you swipe quick enough you get this queue of 5 full and another image requests are ignored.
And this is It. You scroll, you start loading few images and the new displayed images are ignored. When you stop after while all AsyncTasks end and you got some "random" (previosly displaying) image loaded in your list item.
Sollution
This thing was discussed manny times and is solved. It is enough to use for example Picaso library:
https://futurestud.io/blog/picasso-adapter-use-for-listview-gridview-etc/
To add the answers, I would implement an image cache (e.g. as an URL-to-WeakReference-to-image hashmap). The getView() would access that cache and, if the image is not there, leave a request. When the image is loaded, the cache would examine the request list and notify the views that posted the requests (passing them both URL and the image). The views would compare the URL passed in notification to their current URL and either use the image or ignore it (if the view went out of screen or was reused, the image must be ignored).
Why request list. It is possible that multiple views manage to request some image and get reused before the image is loaded (especially if you scroll the list up and down several times).
I am new in android so I am trying to show the images size more than 50 Xhdpi in gridview but while doing this when I execute my program it makes screen hold for sometime and then execute.
Can any one tell the reason why it is taking time.
Here is my code:
mGv_detail = (GridView) findViewById(R.id.gridView1);
init();
key_value = getIntent().getIntExtra("values", 0);
switch (key_value) {
case 0:
mGv_detail.setAdapter(new DetailAdapter(this, first_images));
break;
#Override
public View getView(final int position, View view, ViewGroup parent) {
View v = view;
RecordHolder holder = null;
LayoutInflater mInflater = (LayoutInflater) mContext
.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
if (v == null) {
v = mInflater.inflate(R.layout.items, parent, false);
holder = new RecordHolder();
holder.imagev = (ImageView) v.findViewById(R.id.imag_v);
v.setTag(holder);
} else {
Log.e("Position", "" + position);
holder = (RecordHolder) v.getTag();
}
Bitmap bitmap1 = BitmapFactory.decodeResource(v.getResources(),
items.get(position));
holder.imagev.setImageBitmap(bitmap1);
// holder.imagev.setImageResource(items.get(position));
holder.imagev.setAdjustViewBounds(true);
holder.imagev.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
Intent i = new Intent(mContext, Christmas.class);
i.putExtra("clickposition", position);
Log.e("postiont clicked", position+"");
mContext.startActivity(i);
}
});
return v;
}
static class RecordHolder {
ImageView imagev;
}
}
That's a lot of images. It will lag, even for local image files.
You should use a tool like "Universal Image Loader" -
https://github.com/nostra13/Android-Universal-Image-Loader/blob/master/library/src/com/nostra13/universalimageloader/core/ImageLoader.java
Basically, given that many images, it will take a few moments. I have a GridView with about that many images, and using Universal Image Loader it takes about 3 or 4 seconds even on a fast, new device. However, it will display immediately and you can watch the grid fill with images and it isn't a big deal if you use a tool. If you don't, you really feel the lag.
Your getView method which is resposible for displaying the cells in your GridView is loading your bitmaps into memory synchronously. The delay is very natural given this order of events. If you use one of the many image loading libraries available for Android, this work can be quite easily offloaded to a background thread and called back when ready. My personal favorite is Picasso.
http://square.github.io/picasso/
You can add Picasso as a jar file or via gradle, and then image loading would look something like this:
int resId = images.get(position);
Picasso.with(getContext()).load(resId).into(holder.imagev);
I have a ListView with thumbnail images. All visible rows in the ListView don't have problem.
But for those new rows below the visible ones, even though I tried not to assign any images to those thumbnail ImageViews, images started from the first row are copied exactly the same order as in visible rows. I set breakpoints at those lines of codes assigning the images at thumbnail ImageViews, no breakpoints are hit but still get the images. What is the theory behind? And how can I stop assigning images automatically at the rows below the visible ones.
Thanks
EDIT1:
public View getView(int position, View convertView, ViewGroup parent) {
View vi=convertView;
ViewHolder viewHolder=new ViewHolder();
LayoutInflater inflater = (LayoutInflater) this.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if(vi==null){
vi = inflater.inflate(R.layout.list_row, parent, false);
viewHolder.id=(TextView)vi.findViewById(R.id.title);
viewHolder.thumbnailImage=(ImageView)vi.findViewById(R.id.list_image);
viewHolder.activationStatus = (TextView)vi.findViewById(R.id.activated);
//lazy load image
BitmapWorkerTask task = new BitmapWorkerTask(viewHolder.thumbnailImage);
//if beyond visible rows, position
//becomes zero again, at that time cnt is not zero
//so task is not executed, to prevent image assignment
//for rows below the visible ones
if(position == cnt){
String id = listIDs.get(position);
task.execute(id);
cnt++;
}else{
cnt = 0;
}
//Lazy image update
class BitmapWorkerTask extends AsyncTask<String, Void, Bitmap> {
private final WeakReference<ImageView> imageViewReference;
public BitmapWorkerTask(ImageView imageView) {
// Use a WeakReference to ensure the ImageView can be garbage collected
imageViewReference = new WeakReference<ImageView>(imageView);
}
// Decode image in background.
#Override
protected Bitmap doInBackground(String... params) {
Bitmap bitmap = null;
dbHelper.open();
byte[] img_bytes = dbHelper.getImagebyIDnumber(params[0]);
bitmap = BitmapFactory.decodeByteArray(img_bytes, 0, img_bytes.length);
dbHelper.close();
return bitmap;
}
// Once complete, see if ImageView is still around and set bitmap.
#Override
protected void onPostExecute(Bitmap bitmap) {
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
if (imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}
Check your Layout. Perhaps you settet android:src = "#drawable..." by default for your images.
Configure your listView data in your adaper.
EDITED 22.08.
try to use this:
if(vi==null){
...
//findView by ID here;
...
vi.setTag(viewHolder);
} else {
viewHolder = ( ViewHolder ) vi.getTag();
}
//to do what you want here;
//setting values etc.
You can find good explanation 'how to work with ViewHolder' here ViewHolder Pattern
This is because ListView reuses views getting invisible for visible views. If you want to hide images, you need to do this explicitly in getView() method of your adapter.
I am trying to load images dynamically in a list view in android. The code below loads the image according to the viewed position and loads the image in that array position. However, as it gets the data in background, sometimes the image in the first array is loaded in the second imageview position, second in third etc. I guess when the getDataInBackground part of the first item in the array is already finished the sytem is trying to create the second cell at that time and loads it to the second cell. How can I handle this, I am using android studio AVD with version kitkat and nexus 5 emulator.
private class MyListAdapter extends ArrayAdapter<String> {
public MyListAdapter() {
super(getActivity().getApplicationContext(), R.layout.fragment_users_cell, myItemList);
}
#Override
public View getView(final int position, View convertView, ViewGroup parent) {
View cellView = convertView;
if (cellView == null){
cellView = getActivity().getLayoutInflater().inflate(R.layout.fragment_users_cell, parent, false);
}
cellProfileImage = (ImageView) cellView.findViewById(R.id.fragment_users_cell_profileImg);
System.out.println("The current position" + position);
if (resultsImageFiles.get(position)==null) {
Bitmap image = BitmapFactory.decodeResource(getActivity().getResources(), R.drawable.ph2);
cellProfileImage.setImageBitmap(image);
} else {
ParseFile file = resultsImageFiles.get(position);
file.getDataInBackground(new GetDataCallback() {
#Override
public void done(byte[] data, ParseException e) {
if (e == null) {
Bitmap image = BitmapFactory.decodeByteArray(data, 0, data.length);
cellProfileImage.setImageBitmap(image);
}
}
});
}
return cellView;
}
}
Listview reuse convertview --> Some itemView has same value for view. In this case, you have to remove cellProfileImage before Bitmap loaded.
try to remove all cellProfileImage.setImageBitmap(image); and move it above return cellView; don't forget to declare Bitmap image after View cellView = convertView; but you can also create a class viewHolder that contain your imageview like this link:http://lucasr.org/2012/04/05/performance-tips-for-androids-listview/