ListView recycles Bitmap without noticing in lrucache - android

I'm writing a gallery app.
It works from the androidstudio template for list fragment, with an AbsList.
I override getView to use a task and an lrucache to cache some bitmaps.
Each view from the listview is a RelativeLayout with an ImageView above a TextView.
If there is no Bitmap in the cache, then an AsyncTask loads it and puts it into the cache and getView draws a resource on the ImageView.
After it is loaded, onPostExecute puts the bitmap into the ImageView.
If there is a corresponding Bitmap on the cache, the it is set into the ImageView
I set an object holding the TextView and the ImageView along with an id into the getView's convertView parameter tag to keep track of the correct Bitmap to draw.
I have these two problems, though:
When I scroll down the first time, the new Image views appear with a previous bitmap for an instant before the task finishes setting up the correct bitmap (even though I draw a resource Bitmap on the adapter's getView) I don't understand why.
When I scroll back, most times the app crashes because the Bitmap on the cache turns out to be recycled, though I have no idea who recycled it.
Can anyone help me understand what happens here?
public View getView(int position, View convertView, ViewGroup parent) {
Log.i(TAG, "getView: Asking for view " + position);
GalleryItemViewHolder lViewHolder;
if (convertView == null) {
convertView = getActivity().getLayoutInflater().inflate(R.layout
.gallery_item, null);
lViewHolder = new GalleryItemViewHolder();
convertView.setTag(lViewHolder);
} else {
lViewHolder = (GalleryItemViewHolder) convertView.getTag();
}
lViewHolder.setId(position);
lViewHolder.setTextView((TextView) convertView.findViewById(R.id.gallery_infoTextView));
lViewHolder.setImageView((ImageView) convertView.findViewById(R.id.gallery_imageView));
lViewHolder.getTextView().setText(getItem(position).getName() + ": (" + getItem
(position).getCount() + ")");
if (!getItem(position).paintCover(lViewHolder.getImageView())) {
Log.i(TAG,"getView: task");
new GalleryItemTask(position, lViewHolder)
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null);
}
Log.i(TAG,"getView: return");
return convertView;
}
The Cover class has this paintCover method, where the mId is the uri/stream to the image
public boolean paintCover(ImageView imageView) {
Bitmap lBitmap;
if (mId == null || (lBitmap = BitmapCacheManager.getInstance().get(mId)) == null) {
i(TAG, "paintCover: Sin Cache ");
imageView.getHeight();
imageView.getWidth();
imageView.setImageResource(android.R.drawable.alert_dark_frame);
return false;
} else
{
i(TAG, "paintCover: En Cache "+lBitmap.isRecycled());
imageView.setImageBitmap(lBitmap);
return true;
}
}
More detail.
At the Fragment's onCreate, I run this method:
private void prepareGalleryLoaders() {
LoaderManager lm = getLoaderManager();
Log.i(TAG, "prepareGalleryLoaders: Iniciando loader");
lm.initLoader(IdConstants.GALLERY_LOADER, null, new GalleryLoaderCallbacks());
}
/**
* Callbacks para cargar los datos de las galerías
* Al terminar de cargarlas, se crea el nuevo arreglo
*/
private class GalleryLoaderCallbacks implements LoaderManager.LoaderCallbacks<List<Gallery>> {
#Override
public Loader<List<Gallery>> onCreateLoader(int id, Bundle args) {
return new GalleriesLoader(getActivity());
}private class GalleryItemTask extends AsyncTask<Void, Void, Gallery> {
private static final String TAG = "GalleryItemTask";
private int mId;
private String mCoverId;
private GalleryItemViewHolder mViewHolder;
private Bitmap mBitmap;
GalleryItemTask(int id, GalleryItemViewHolder galleryItemViewHolder) {
mViewHolder = galleryItemViewHolder;
mId = id;
}
#Override
protected void onPostExecute(Gallery galleries) {
if (mId != mViewHolder.getId()) {
Log.i(TAG, "onPostExecute: IDs difieren!!! "+mId+" - "+mViewHolder.getId());
mBitmap.recycle();
mBitmap=null;
return;
}
// Validar y actualizar bitmap
mViewHolder.getImageView().setImageBitmap(mBitmap);
//mGalleries.get(mId).setBitmap(mBitmap);
super.onPostExecute(galleries);
}
#Override
protected Gallery doInBackground(Void... params) {
// generar bitmap (y posiblemente agregarlo a algún cache)
String[] queryProjection = {
MediaStore.Images.ImageColumns.DATA, MediaStore.Images.ImageColumns.TITLE};
String[] selectionArgs = new String[]{String.valueOf(mGalleries.get(mId).getId())};
Cursor lCursor = getView().getContext().getContentResolver().query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
queryProjection, MediaStore.Images.ImageColumns.BUCKET_ID + "= ?",
selectionArgs, MediaStore.Images.ImageColumns.TITLE);
lCursor.moveToFirst();
while (!lCursor.isAfterLast()) {
//Log.i(TAG,"doInBackground: "+mGalleries.get(mId).getName()+" - "+lCursor.getString
// (1)+" - "+ lCursor.getString(0));
lCursor.moveToNext();
}
lCursor.moveToFirst();
Log.i(TAG, "doInBackground: " + mId + " - " + mViewHolder.getId());
BitmapFactory.Options lOptions = new BitmapFactory.Options();
lOptions.inJustDecodeBounds = true;
mBitmap = BitmapFactory.decodeFile(lCursor.getString(0), lOptions);
lOptions.inSampleSize = ImageUtils.calculateInSampleSize(lOptions, 256, 256);
lOptions.inJustDecodeBounds = false;
mBitmap = BitmapFactory.decodeFile(lCursor.getString(0), lOptions);
BitmapCacheManager.getInstance().put(lCursor.getString(0), mBitmap);
//if(mGalleries.get(mId).getBitmap()!=null)
// mGalleries.get(mId).getBitmap().recycle();
//mGalleries.get(mId).setBitmap(mBitmap);
if(!mGalleries.get(mId).hasCover()) {
SimpleCover lSimpleCover=new SimpleCover(getActivity(),lCursor.getString(0));
mGalleries.get(mId).setCover(lSimpleCover);
}
lCursor.close();
return null;
}
}
#Override
public void onLoadFinished(Loader<List<Gallery>> loader, List<Gallery> data) {
if (mGalleries != null) {
mGalleries.clear();
} else
mGalleries = new ArrayList<Gallery>();
mGalleries.addAll(data);
for (Gallery lGallery : data) {
Log.i(TAG, "onLoadFinished: " + lGallery.getName());
}
mAdapter.notifyDataSetChanged();
}
At this point, there are no covers defined, the gallery list is just loaded with titles and total contents and id data. The images (covers) are loaded at getView from the list adapter.
The GalleryItemTask class:
private class GalleryItemTask extends AsyncTask<Void, Void, Gallery> {
private static final String TAG = "GalleryItemTask";
private int mId;
private String mCoverId;
private GalleryItemViewHolder mViewHolder;
private Bitmap mBitmap;
GalleryItemTask(int id, GalleryItemViewHolder galleryItemViewHolder) {
mViewHolder = galleryItemViewHolder;
mId = id;
}
#Override
protected void onPostExecute(Gallery galleries) {
if (mId != mViewHolder.getId()) {
Log.i(TAG, "onPostExecute: IDs difieren!!! "+mId+" - "+mViewHolder.getId());
mBitmap.recycle();
mBitmap=null;
return;
}
// Validar y actualizar bitmap
mViewHolder.getImageView().setImageBitmap(mBitmap);
//mGalleries.get(mId).setBitmap(mBitmap);
super.onPostExecute(galleries);
}
#Override
protected Gallery doInBackground(Void... params) {
// generar bitmap (y posiblemente agregarlo a algún cache)
String[] queryProjection = {
MediaStore.Images.ImageColumns.DATA, MediaStore.Images.ImageColumns.TITLE};
String[] selectionArgs = new String[]{String.valueOf(mGalleries.get(mId).getId())};
Cursor lCursor = getView().getContext().getContentResolver().query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
queryProjection, MediaStore.Images.ImageColumns.BUCKET_ID + "= ?",
selectionArgs, MediaStore.Images.ImageColumns.TITLE);
lCursor.moveToFirst();
while (!lCursor.isAfterLast()) {
//Log.i(TAG,"doInBackground: "+mGalleries.get(mId).getName()+" - "+lCursor.getString
// (1)+" - "+ lCursor.getString(0));
lCursor.moveToNext();
}
lCursor.moveToFirst();
Log.i(TAG, "doInBackground: " + mId + " - " + mViewHolder.getId());
BitmapFactory.Options lOptions = new BitmapFactory.Options();
lOptions.inJustDecodeBounds = true;
mBitmap = BitmapFactory.decodeFile(lCursor.getString(0), lOptions);
lOptions.inSampleSize = ImageUtils.calculateInSampleSize(lOptions, 256, 256);
lOptions.inJustDecodeBounds = false;
mBitmap = BitmapFactory.decodeFile(lCursor.getString(0), lOptions);
BitmapCacheManager.getInstance().put(lCursor.getString(0), mBitmap);
//if(mGalleries.get(mId).getBitmap()!=null)
// mGalleries.get(mId).getBitmap().recycle();
//mGalleries.get(mId).setBitmap(mBitmap);
if(!mGalleries.get(mId).hasCover()) {
SimpleCover lSimpleCover=new SimpleCover(getActivity(),lCursor.getString(0));
mGalleries.get(mId).setCover(lSimpleCover);
}
lCursor.close();
return null;
}
}

When I scroll down the first time, the new Image views appear with a
previous bitmap for an instant before the task finishes setting up the
correct bitmap (even though I draw a resource Bitmap on the adapter's
getView) I don't understand why.
It should be because you put notifyDataSetChanged() on the wrong place. Please post the code where you put it.
When I scroll back, most times the app crashes because the Bitmap on
the cache turns out to be recycled, though I have no idea who recycled
it.
I think its because you don't specify what to do if the paintCover is true :
if (!getItem(position).paintCover(lViewHolder.getImageView())) {
Log.i(TAG,"getView: task");
new GalleryItemTask(position, lViewHolder)
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null);
}
else
{
//what should the adapter do if paintCover is true?
}
If the error still exist, please post your GalleryItemTask code.

Related

Make adapter more efficient

I tried to make a custom Adapter and I made it functionall maybe not the best way and may be not the intelligentes Way so I ask here what I can do to make this a little more efficient
public class MovieDataAdapter extends BaseAdapter implements FetchImage.AsyncResponse {
private Context mContext;
public MovieDataAdapter(Context context) {
mContext = context;
}
#Override
public int getCount() { // get coutn Method
SQLiteDatabase db = new MvDBHelper(mContext).getReadableDatabase();
Cursor cur = db.query(MovieContract.MovieEntry.TABLE_NAME, null, null, null, null, null, null);
return cur.getCount();
}
#Override
public Object getItem(int position) {
return null;
}//Not needed at the moment
#Override
public long getItemId(int position) {
return 0;
}// Not needed at the moment
#Override
public View getView(int position, View convertView, ViewGroup parent) {
final ImageView imageview;
if (convertView != null) {
imageview = (ImageView) convertView; //if used view just use ist again
} else {//if new view set the fitting parameters
imageview = new ImageView(mContext);
imageview.setLayoutParams(new GridView.LayoutParams(getw('w'), getw('h')));
imageview.setScaleType(ImageView.ScaleType.FIT_XY);
}
SQLiteDatabase picturedb = new MvDBHelper(mContext).getReadableDatabase();
Cursor cur = picturedb.query(MovieContract.MovieEntry.TABLE_NAME,
null, null, null, null, null, null
);//get the entries from the db
if (cur != null && cur.moveToFirst()) {
cur.moveToPosition(position); // move to the appropriate position
//defining nessesary Variables
int index_PosterPath = cur.getColumnIndex(MovieContract.MovieEntry.COL_POSTERPATH);
int index_FilePath = cur.getColumnIndex(MovieContract.MovieEntry.COL_FILE);
int index_ortTitel = cur.getColumnIndex(MovieContract.MovieEntry.COL_ORTITEL);
final String Filename = cur.getString(index_ortTitel) + ".jpg";
final String selection = MovieContract.MovieEntry.COL_ORTITEL + " = ?";
final String[] where = {cur.getString(index_ortTitel)};
picturedb.close();// db not needed so is closed
if (cur.isNull(index_FilePath)) {//if file not already saved in the storage save it there
FetchImage getImage = new FetchImage(mContext, new FetchImage.AsyncResponse() {
#Override
public void processfinished(Bitmap output) { // get the image as an Bitmap in asynchronus task throug interface callback
FileOutputStream fos = null;
try {
fos = mContext.openFileOutput(Filename, Context.MODE_PRIVATE);
if (fos != null)
output.compress(Bitmap.CompressFormat.PNG, 100, fos); //put bitmap in file
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
ContentValues values = new ContentValues();
values.put(MovieContract.MovieEntry.COL_FILE, Filename);
SQLiteDatabase picwr = new MvDBHelper(mContext).getWritableDatabase();
int updated = picwr.update(MovieContract.MovieEntry.TABLE_NAME, values, selection, where);
//put the filname in the db for later use
picwr.close();
}
BitmapDrawable draw = new BitmapDrawable(mContext.getResources(), output);
Drawable gridimag = draw;
imageview.setImageDrawable(gridimag); // set the drawable as an image
}
});
String[] ptg = {cur.getString(index_PosterPath)};
getImage.execute(ptg);
} else { // if pic already saved in the internal storage get it from there
FileInputStream fis = null;
try {
fis = mContext.openFileInput(Filename);
Bitmap pic = BitmapFactory.decodeStream(fis);
BitmapDrawable draw = new BitmapDrawable(mContext.getResources(), pic);
Drawable gridimag = draw;
imageview.setImageDrawable(gridimag);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
cur.close();
}
return imageview;
}
public int getw(char c) {
int DisplayWidth = mContext.getResources().getDisplayMetrics().widthPixels;
if (c == 'w') {
return (int) (DisplayWidth / 2);
} else {
return DisplayWidth;
}
}
public static float convertDpToPixel(float dp, Context context) {
Resources resources = context.getResources();
DisplayMetrics metrics = resources.getDisplayMetrics();
float px = dp * ((float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT);
return px;
}
#Override
public void processfinished(Bitmap output) {
}
}
Would be happy about every help I can get even if it's a complet new Way because at the moment the grid view does not work fluently
You should avoid close cursor each time you draw a cell. You can close it only when you done with your activity.
Also you need to avoid using cur.moveToFirst(). Because you are already moving your cursor to given position, why should i moving it first before?
Another trick to use a image loader library (Picasso is one of the best) to load it async and faster. It also handles to stop threads if view is not shown. More optimized.
You dont need to find indexs of coloumn each time you want a new cell. Find them once, use them everytime :)
Also you can use CursorAdapter class to implement it way better :)

Images from server does not show in listview

Hello I am using listview and display images from server into it.
But the problem is that all loaded images are displayed in last item one by one instead display on respective position.
please help me to display that images in respective position
public class Offer_adapter extends ArrayAdapter<String> {
Context context1;
String[] offer_title;
String[] offerimg1;
String[] mrp;
String[] offerprice;
String[] you_save;
String[] imgURLArray;
Bitmap bitmap;
ImageView offerimg;
int a;
LayoutInflater inflater1;
public Offer_adapter(Context context1, String[] offer_title, String[] offerimg1, String[] mrp, String[] you_save, String[] offerprice) {
super(context1, R.id.offer_list, offer_title);
this.context1 = context1;
this.offer_title = offer_title;
this.offerimg1 = offerimg1;
this.mrp = mrp;
this.offerprice = offerprice;
this.you_save = you_save;
}
private static class ViewHolder {
String offerimg;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder = null;
if (convertView == null) {
inflater1 = (LayoutInflater) context1.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater1.inflate(R.layout.offer_list, null);
viewHolder = new ViewHolder();
}
android.util.Log.v("abhi", "" + position);
imgURLArray = new String[position + 1];
for (int i = 0; i <= position; i++) {
android.util.Log.v("abhijit", "" + position);
imgURLArray[i] = "http://www.surun.co/preost/mod_offer/images/" + offerimg1[position];
android.util.Log.v("abhi", "" + imgURLArray[position]);
}
a=position;
viewHolder = (ViewHolder) convertView.getTag();
TextView offertitle = (TextView) convertView.findViewById(R.id.ofrtitle);
TextView offermrp = (TextView) convertView.findViewById(R.id.offeroriginal);
offermrp.setPaintFlags(offermrp.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
TextView offersave = (TextView) convertView.findViewById(R.id.saveoffer);
TextView ofrprice = (TextView) convertView.findViewById(R.id.priceoffer);
offerimg = (ImageView) convertView.findViewById(R.id.ofr_img);
offertitle.setText(offer_title[position]);
offermrp.setText("Original Price: \u20B9" + mrp[position]);
offersave.setText("You Save: \u20B9" + you_save[position]);
ofrprice.setText("Offer Price: \u20B9" + offerprice[position]);
// Bitmap imageBitmap = null;
new DownloadAsyncTask().execute(imgURLArray[position]);
Log.v("abhi","async");
return convertView;
}
private class DownloadAsyncTask extends AsyncTask<String, String, Bitmap> {
protected Bitmap doInBackground(String... args) {
try {
Log.v("abhi","in do background");
bitmap = BitmapFactory.decodeStream((InputStream) new URL(args[0]).getContent());
bitmap = Bitmap.createScaledBitmap(bitmap, 270, 375, true);
} catch (Exception e) {
e.printStackTrace();
}
return bitmap;
}
#Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
if (bitmap !=null) {
offerimg.setImageBitmap(bitmap);
} else {
offerimg.setImageResource(R.drawable.nooffer);
}
}
}
}
All images are shown in last item one after another. I just want to show it on respective position.
you should use lazyloading instead of downloading image as bitmap.
Bitmap will create problem sometime or will give you outofmemory error in some devices
There are lots of image loading library available for android.
Have a look at these
https://github.com/square/picasso
https://github.com/nostra13/Android-Universal-Image-Loader
https://code.google.com/p/android-query/wiki/ImageLoading
https://android.googlesource.com/platform/frameworks/volley
https://github.com/koush/UrlImageViewHelper
https://github.com/novoda/image-loader

Android: async loaded bitmaps randomly don't appear in ImageView after ImageView.setBitmapDrawable(..)

I have a ListView whose elements are photos I need to load from a remote API which I'm using loopj to access.
After loopj downloads the photo data, I use an AsyncTask to process the data into a scaled bitmap.
This usually works great, however..
The problem
In my AsyncTask's onPostExecute method, I call setImageBitmap on the ImageView. Occasionally, the bitmap simply doesn't appear in the ImageView after ImageView.setImageBitmap is called. I have verified through logs and also the debugger that ImageView.setImageBitmap is indeed called on the offending ImageView.
If I scroll the list a bit, the list element redraws, and the image is loaded from memory cache and correctly appears on the ImageView.
Here's my code:
public void setPersonImage(final ImageView personImageView, final Person p) {
personImageView.setImageResource(R.drawable.empty_image);
//kills unecessary requests for the imageview's previous occupant photo
if(personImageView.getTag() != null) {
AsyncHttpClient client = mPhotoRequestsMap.get(p.getEmployeeId());
if(client != null) {
client.cancelRequests(getActivity().getApplicationContext(), true);
Log.d(TAG, "Cancled photo request for " + String.valueOf(p.getEmployeeId()));
}
}
personImageView.setTag(p.getEmployeeId());
//first attempt to grab photo from cache
Bitmap cachedBitmap = mMemoryCache.get(p.getPhotoCacheKey());
if(cachedBitmap != null) {
personImageView.setImageBitmap(cachedBitmap);
Log.d(TAG, "bitmap for " + String.valueOf(p.getEmployeeId()) + " retrieved from memory cache");
} else {
AsyncHttpClient client = ApiWrapper.getPersonImage(180,mUserSession,
getActivity().getApplicationContext(), p.getEmployeeId(),
new MyAsyncHttpResponseHandler(this) {
#Override
public void onSuccess(String response) {
if(personImageView.getTag().equals(p.getEmployeeId())) {
// use async task to process the bitmap off the UI thread
AsyncTask<String,Integer,Bitmap> task = new AsyncTask<String,Integer,Bitmap>() {
#Override
protected Bitmap doInBackground(String... params) {
String response = params[0];
byte[] bitmapBytes = ApiWrapper.processImageResponse(response);
Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapBytes, 0, bitmapBytes.length);
Bitmap bmpScaled = ThumbnailUtils.extractThumbnail(bitmap, 180, 180);
return bmpScaled;
}
protected void onPostExecute(Bitmap bitmap) {
//set on imageview if tags agree
if(personImageView.getTag().equals(p.getEmployeeId())) {
Log.d(TAG, "setting image view for " + p.getEmployeeId());
personImageView.setImageBitmap(bitmap);
mMemoryCache.put(p.getPhotoCacheKey(), bitmap);
}
else {
Log.d(TAG, "tags don't match after decode bitmap, discarding...");
}
}
};
task.execute(response);
}
else {
Log.d(TAG, "tags don't match BEFORE decode bitmap employee id: " + p.getEmployeeId() + " tag: " + personImageView.getTag());
}
}
}
);
//create entry in PhotoRequestsMap table
mPhotoRequestsMap.put(p.getEmployeeId(), client);
} // end if not in cache
}
I've tried calling invalidate() on the ImageView after the bitmap is set on it, but sadly that doesn't solve it..
I'd greatly appreciate any help or pointers.
Update
The getView method from my Adapter:
public View getView(int position, View convertView, ViewGroup parent) {
// If we weren't given a view, inflate one
if (convertView == null) {
convertView = getActivity().getLayoutInflater().inflate(R.layout.list_item_person, null);
}
Person p = getItem(position);
TextView nameTextView = (TextView)convertView.findViewById(R.id.person_ListItemNameTextView);
nameTextView.setText(p.getFirstName() + " " + p.getLastName());
TextView idTextView = (TextView)convertView.findViewById(R.id.person_ListItemIdTextView);
idTextView.setText(String.valueOf(p.getEmployeeId()));
ImageView personImageView = (ImageView)convertView.findViewById(R.id.person_ListItemImageView);
setPersonImage(personImageView, p);
return convertView;
}
I think the .setImageBitmap(Bitmap) method is working fine, you are just setting it with null.
Check for the case of bitmap == null in the onPostExecute(Bitmap bitmap). This also fits to your observation that the image appears when you scroll, because Android calls getView(..) again for the visible rows at that time.

Get the LinearLayout inside a ListView from an Async Task onRetainNonConfigurationInstance()

I hope the title is not mis-leading. I am trying to implement onRetainNonConfigurationInstance() of an AsyncTask of loading images and am getting this error "Java.lang.RuntimeException:Unable to retain Activity". Here is my code for onRetainNonConfigurationInstance():
public Object onRetainNonConfigurationInstance(){
//final ListView listview = list;
final int count = list.getChildCount();
final LoadedImage[] mylist = new LoadedImage[count];
for(int i = 0; i < count; i++){
final ImageView v = (ImageView)list.getChildAt(i); // getting error here
mylist[i] = new LoadedImage(((BitmapDrawable) v.getDrawable()).getBitmap());
}
return mylist;
}
Here is the Logcat:
05-18 08:43:15.385: E/AndroidRuntime(28130): java.lang.RuntimeException: Unable to retain activity {com.MyApps.ImageGen/com.MyApps.ImageGen.Wallpapers}: java.lang.ClassCastException: android.widget.LinearLayout
05-18 08:43:15.385: E/AndroidRuntime(28130): at android.app.ActivityThread.performDestroyActivity(ActivityThread.java:2989)
05-18 08:43:15.385: E/AndroidRuntime(28130): at android.app.ActivityThread.handleDestroyActivity(ActivityThread.java:3100)
05-18 08:43:15.385: E/AndroidRuntime(28130): at android.app.ActivityThread.handleRelaunchActivity(ActivityThread.java:3216)
05-18 08:43:15.385: E/AndroidRuntime(28130): at android.app.ActivityThread.access$1600(ActivityThread.java:132)
Here is my layout with the ListView:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<ListView
android:id="#android:id/list"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:cacheColorHint="#00000000"
android:listSelector="#android:color/transparent" >
</ListView>
</LinearLayout>
Here is how I setup the ListView:
private void setupViews() {
list = (ListView)findViewById(android.R.id.list);
list.setDivider(null);
list.setDividerHeight(0);
imageAdapter = new ImageAdapter(getApplicationContext());
list.setAdapter(imageAdapter);
list.setOnItemClickListener(this);
}
my async task and Item Adapter:
class LoadImagesFromSDCard extends AsyncTask<Object, LoadedImage, Object> {
#Override
protected Object doInBackground(Object... params) {
Bitmap bitmap = null;
Bitmap newbitmap = null;
Uri uri = null;
String [] img = {MediaStore.Images.Media._ID};
String state = Environment.getExternalStorageState();
if(Environment.MEDIA_MOUNTED.equals(state)){
cursor = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, img, null, null, null);
} else {
// cursor = getContentResolver().query(MediaStore.Images.Media.INTERNAL_CONTENT_URI, img, null, null, null);
inInternalStorage = true;
}
int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID);
int size = cursor.getCount();
if(size == 0){
//Toast.makeText(getApplicationContext(), "There are no Images on the sdcard", Toast.LENGTH_SHORT).show();
//System.out.println("size equals zero");
Wallpaper.this.runOnUiThread(new Runnable() {
public void run() {
Toast.makeText(getApplicationContext(), "There are no Images on the sdcard", Toast.LENGTH_SHORT).show();
}
});
return params;
}else {
}
for(int i = 0; i < size; i++){
cursor.moveToPosition(i);
int ImageId = cursor.getInt(column_index);
if(inInternalStorage == true){
uri = Uri.withAppendedPath(MediaStore.Images.Media.INTERNAL_CONTENT_URI, "" + ImageId);
}else {
uri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "" + ImageId);
}
try {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(getContentResolver().openInputStream(uri), null, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;
Log.i(TAG, "imageheight = " + imageHeight);
Log.i(TAG, "imagewidth = " + imageWidth);
Log.i(TAG, "imageType = " + imageType);
//options.inSampleSize=4;
options.inSampleSize = calculateInSampleSize(options, imageWidth, imageHeight);
options.inJustDecodeBounds = false;
//bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(uri));
bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(uri), null, options);
if(bitmap != null){
newbitmap = Bitmap.createScaledBitmap(bitmap, 180, 180, true);
bitmap.recycle();
}
if(newbitmap != null){
publishProgress(new LoadedImage(newbitmap));
}
}catch(IOException e){
}
//cursor.close();
}
cursor.close();
return null;
}
#Override
public void onProgressUpdate(LoadedImage... value){
addImage(value);
}
#Override
protected void onPostExecute(Object result) {
setProgressBarIndeterminateVisibility(false);
}
}
private static class LoadedImage {
Bitmap mBitmap;
LoadedImage(Bitmap bitmap) {
mBitmap = bitmap;
}
public Bitmap getBitmap() {
return mBitmap;
}
}
/*Image Adapter to populate grid view of images*/
public class ImageAdapter extends BaseAdapter {
private Context mContext;
private ArrayList<LoadedImage> photos = new ArrayList<LoadedImage>();
public ImageAdapter(Context context){
this.mContext = context;
}
public void addPhotos(LoadedImage photo){
photos.add(photo);
}
#Override
public int getCount() {
return photos.size();
}
#Override
public Object getItem(int position) {
return position;
}
#Override
public long getItemId(int position) {
return position;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
ImageView image;
ViewHolder holder;
if(convertView == null){
LayoutInflater inflater = (LayoutInflater)mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
//convertView = inflater.inflate(R.layout.image_list,null);
convertView = inflater.inflate(R.layout.media_view, null);
holder = new ViewHolder();
holder.image = (ImageView)convertView.findViewById(R.id.media_image_id);
//holder.item_name = (TextView)convertView.findViewById(R.id.media_image_id);
convertView.setTag(holder);
} else{
holder = (ViewHolder)convertView.getTag();
}
//holder.image.setLayoutParams(new GridView.LayoutParams(100, 100));
//String item_name = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME));
holder.image.setImageBitmap(photos.get(position).getBitmap());
//holder.item_name.setText(item_name);
return convertView;
}
}
static class ViewHolder {
ImageView image;
TextView item_name;
}
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Cursor cursor = null;
int image_column_index = 0;
String[] proj = {MediaStore.Images.Media.DATA};
String state = Environment.getExternalStorageState();
if(Environment.MEDIA_MOUNTED.equals(state)){
cursor = managedQuery(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,proj,null, null,null);
}else{
cursor = managedQuery(MediaStore.Images.Media.INTERNAL_CONTENT_URI,proj,null, null,null);
}
cursor.moveToPosition(position);
image_column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
String info = cursor.getString(image_column_index);
Intent imageviewer = new Intent(getApplicationContext(), ViewImage.class);
imageviewer.putExtra("pic_name", info);
startActivity(imageviewer);
cursor.close();
}
public static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 2;
if (height > reqHeight || width > reqWidth) {
if (width > height) {
inSampleSize = Math.round((float)height / (float)reqHeight);
} else {
inSampleSize = Math.round((float)width / (float)reqWidth);
}
}
/*final int REQUIRED_SIZE = 180;
int scale = 1;
if(reqWidth > REQUIRED_SIZE || reqHeight > REQUIRED_SIZE){
scale = (int)Math.pow(2, (int)Math.round(Math.log(REQUIRED_SIZE/(double)Math.max(reqHeight, reqWidth)) / Math.log(0.5)));
}*/
return inSampleSize;
}
other Methods within Async Task:
private void loadImages() {
final Object data = getLastNonConfigurationInstance();
if(data == null){
new LoadImagesFromSDCard().execute();
}else {
final LoadedImage[] photos = (LoadedImage[])data;
if(photos.length == 0){
new LoadImagesFromSDCard().execute();
}
for(LoadedImage photo:photos){
addImage(photo);
}
}
}
private void addImage(LoadedImage... value) {
for(LoadedImage photo: value){
imageAdapter.addPhotos(photo);
imageAdapter.notifyDataSetChanged();
}
}
I am assuming I should first get the root element before I get the ListView from the logcat error, but I don't know how, I can't seem to find a suitable method from the documentation.
P.S: I am using an Activity and not a ListActivity.
The exception tells you that you try to do an invalid cast. My guess is that your row layout isn't a simple ImageView like you assume with the cast in the onRetainNonConfigurationInstance(), instead your row layout probably has a parent Linearlayout that wraps the ImageView(and other views?!). When you call the method getChildAt you get that parent Linearlayout which you try to cast to a ImageView. Instead you should do this:
//...
for(int i = 0; i < count; i++){
LinearLayout parent = (LinearLayout)list.getChildAt(i);
final ImageView v = (ImageView) parent.findViewById(R.id.the_id_of_the_imageview_from_theRow_layout);
mylist[i] = new LoadedImage(((BitmapDrawable) v.getDrawable()).getBitmap());
}
Note: The getChildAt method returns the rows views for the visible rows only so if you try to access the View for a row that isn't currently visible you'll most likely end up with a NullPointerException(you should get the drawables directly from the adapter and not from parsing all the rows Views).
Edit based on the comments:
If I understood your problem, the behavior you see it's normal. You didn't say how you ended up getting the bitmaps in the onRetainNonConfigurationInstance but my guess is that you just save the images from the ListView rows that are currently visible to the user. When it's time to restore the adapter with the images from getLastNonConfigurationInstance() you end up getting only those images which were previously visible. This will get worse if you rotate the phone again as, most likely, there are fewer images visible in landscape orientation then in the portrait orientation.
I don't know if this will work but you could try to modify the LoadedImage class to store the id of the image from MediaStore.Images.Media. When it's time to save the configuration, stop the LoadImagesFromSDCard task and nullify all the images(remove the Bitmap to which LoadedImage points)from the LoadedImages ArrayList(in the adapter) except the ones that are currently visible to the user. Send the photos ArrayList from your adapter through
onRetainNonConfigurationInstance.
When it's time to restore the adapter set the photos ArrayList of your new adapter to the one you got back and put the ListView to the position of the visible images(so the user sees the sent images). In the getView method of your adapter you'll put a default image(like loading...) when you have a null value for the Bitmap in the associated LoadedImage class. Start a new LoadImagesFromSDCard task to get the images and parse them again. When you get the id from MediaStore.Images.Media you'll check if a LoadedImage with this id exists in the adapter and if it has a Bitmap set to it. If it doesn't exist(or it doesn't have a Bitmap associated) then load the image and put it in the adapter, if not the image already exists and move to the next one.
I don't know if the solution above will work or if it's efficient. I would recommend you to modify the code and load images on demand instead of loading all up in a big task.

Android List view update

I am struggling to update a list view with data from a database, this works nicely by using a SimpleCursorAdapter. But the image view on the rows is not updated on the activity start, I have to scroll through the list a few times and only then the images are loaded in the image view.
This is the binder i am using for the SimpleCursorAdapter:
private class PromotionViewBinder implements SimpleCursorAdapter.ViewBinder {
private int done;
public boolean setViewValue(View view, Cursor cursor, int index) {
Log.e(""+cursor.getCount(),"");
View tmpview = view;
if (index == cursor.getColumnIndex(PromotionsTable.SEEN_COL)) {
boolean read = cursor.getInt(index) > 0 ? true : false;
TextView title = (TextView) tmpview;
if (!read) {
title.setTypeface(Typeface.DEFAULT_BOLD, 0);
} else {
title.setTypeface(Typeface.DEFAULT);
}
return true;
} else if (tmpview.getId() == R.id.promotions_list_row_image){
String imageURL = cursor.getString(index);
Log.e("",imageURL);
imageRetriever.displayImage(imageURL, (ImageView)tmpview);
return true;
} else {
return false;
}
}
}
The image retriever class is the LazyList example from here. As you will see this is using a runnable to retrieve the images and once the task is done is automatically updating the given imageView...Do you think that the reference to the imageView is lost somewhere on the way?
Thanx in advance,
Nick
package com.tipgain.promotions;
The image retriever class:
/**
* This class is used for retrieving images from a given web link. it uses local
* storage and memory to store the images. Once a image is downloaded
* successfully the UI gets updated automatically.
*
*
*/
public class ImageRetriever {
private final String TAG = ImageRetriever.class.getName();
private MemoryImageCache memoryImgCache = new MemoryImageCache();
private LocalStorageImageCache localFileCache;
private Map<ImageView, String> imageViewHolders = Collections
.synchronizedMap(new WeakHashMap<ImageView, String>());
private ExecutorService execService;
final int defaultImageID = R.drawable.photo_not_available;
public ImageRetriever(Context context) {
localFileCache = new LocalStorageImageCache(context);
execService = Executors.newFixedThreadPool(5);
}
public void displayImage(String url, ImageView imageView) {
imageViewHolders.put(imageView, url);
Bitmap bmp = memoryImgCache.retrieve(url);
if (bmp != null) {
Log.e("case 1", " " + (bmp != null));
imageView.setImageBitmap(bmp);
} else {
Log.e("case 2", " " + (bmp == null));
addImageToQueue(url, imageView);
imageView.setImageResource(defaultImageID);
}
}
private void addImageToQueue(String url, ImageView imageView) {
NextImageToLoad img = new NextImageToLoad(url, imageView);
execService.submit(new ImagesRetriever(img));
}
/**
* This method is used for retrieving the Bitmap Image.
*
* #param url
* String representing the url pointing to the image.
* #return Bitmap representing the image
*/
private Bitmap getBitmap(String url) {
File imageFile = localFileCache.getFile(url);
// trying to get the bitmap from the local storage first
Bitmap bmp = decodeImageFile(imageFile);
if (bmp != null)
return bmp;
// if the file was not found locally we retrieve it from the web
try {
URL imageUrl = new URL(url);
HttpURLConnection conn = (HttpURLConnection) imageUrl
.openConnection();
conn.setConnectTimeout(30000);
conn.setReadTimeout(30000);
conn.setInstanceFollowRedirects(true);
InputStream is = conn.getInputStream();
OutputStream os = new FileOutputStream(imageFile);
Utils.CopyStream(is, os);
os.close();
bmp = decodeImageFile(imageFile);
return bmp;
} catch (MalformedURLException e) {
Log.e(TAG, e.getMessage());
} catch (FileNotFoundException e) {
Log.e(TAG, e.getMessage());
} catch (IOException e) {
Log.e(TAG, e.getMessage());
}
return null;
}
/**
* This method is used for decoding a given image file. Also, to reduce
* memory, the image is also scaled.
*
* #param imageFile
* #return
*/
private Bitmap decodeImageFile(File imageFile) {
try {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(new FileInputStream(imageFile), null,
options);
// Find the correct scale value. It should be the power of 2.
// Deciding the perfect scaling value. (^2).
final int REQUIRED_SIZE = 100;
int tmpWidth = options.outWidth, tmpHeight = options.outHeight;
int scale = 1;
while (true) {
if (tmpWidth / 2 < REQUIRED_SIZE
|| tmpHeight / 2 < REQUIRED_SIZE)
break;
tmpWidth /= 2;
tmpHeight /= 2;
scale *= 2;
}
// decoding using inSampleSize
BitmapFactory.Options option2 = new BitmapFactory.Options();
option2.inSampleSize = scale;
return BitmapFactory.decodeStream(new FileInputStream(imageFile),
null, option2);
} catch (FileNotFoundException e) {
Log.e(TAG, e.getLocalizedMessage());
}
return null;
}
private boolean reusedImage(NextImageToLoad image) {
Context c = image.imageView.getContext();
c.getContentResolver().notifyChange(PromotionsProvider.CONTENT_URI, null);
String tag = imageViewHolders.get(image.imageView);
if ((tag == null) || (!tag.equals(image.url)))
return true;
return false;
}
/**
* Clears the Memory and Local cache
*/
public void clearCache() {
memoryImgCache.clear();
localFileCache.clear();
}
/**
* This class implements a runnable that is used for updating the promotions
* images on the UI
*
*
*/
class UIupdater implements Runnable {
Bitmap bmp;
NextImageToLoad image;
public UIupdater(Bitmap bmp, NextImageToLoad image) {
this.bmp = bmp;
this.image = image;
Log.e("", "ui updater");
}
public void run() {
Log.e("ui updater", "ui updater");
if (reusedImage(image))
return;
Log.e("nick", "" + (bmp == null) + " chberugv");
if (bmp != null){
image.imageView.setImageBitmap(bmp);
Context c = image.imageView.getContext();
c.getContentResolver().notifyChange(PromotionsProvider.CONTENT_URI, null);
}else
image.imageView.setImageResource(defaultImageID);
}
}
private class ImagesRetriever implements Runnable {
NextImageToLoad image;
ImagesRetriever(NextImageToLoad image) {
this.image = image;
}
public void run() {
Log.e("images retirever", " images retriever");
if (reusedImage(image))
return;
Bitmap bmp = getBitmap(image.url);
memoryImgCache.insert(image.url, bmp);
if (reusedImage(image))
return;
UIupdater uiUpdater = new UIupdater(bmp, image);
Activity activity = (Activity) image.imageView.getContext();
activity.runOnUiThread(uiUpdater);
//Context c = image.imageView.getContext();
//c.getContentResolver().notifyChange(PromotionsProvider.CONTENT_URI, null);
}
}
/**
* This class encapsulates the image being downloaded.
*
* #author Nicolae Anca
*
*/
private class NextImageToLoad {
public String url;
public ImageView imageView;
public NextImageToLoad(String u, ImageView i) {
url = u;
imageView = i;
}
}
}
Modified Runnable:
class UIupdater implements Runnable {
Bitmap bmp;
NextImageToLoad image;
public UIupdater(Bitmap bmp, NextImageToLoad image) {
this.bmp = bmp;
this.image = image;
}
public void run() {
if (reusedImage(image))
return;
if (bmp != null){
image.imageView.setImageBitmap(bmp);
Context c = image.imageView.getContext();
c.getContentResolver().notifyChange(PromotionsProvider.CONTENT_URI, null);
}else
image.imageView.setImageResource(defaultImageID);
}
}
Thats an interesting way to do what you are doing. Have you tried extending the Simple Cursor Adapter?
What you do is implement a ViewHolder and put your imageview in it.
Then in your ImageRetriever, write a Listener which will be called once the image is ready and retrieved.
Implement this listener in the Viewholder.
You create the view in getView() and request for the image in BindView().
Once the image gets loaded, the list will be refreshed automatically.
one way to do it is by calling notifyDataSetChenged on listview, and another was is to have adapter as member variable and when something changes on listview you call a function that assigns new listadapter to member adapter. That way your list will be redraw on change.
I guess, you have to use some handler, calling after image load, which will call notifyDataSetChanged for list adapter

Categories

Resources