1

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:

  1. 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.

  2. 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;
    }
}
dabicho
  • 383
  • 4
  • 19

1 Answers1

0

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.

Kaushik
  • 6,150
  • 5
  • 39
  • 54
Blaze Tama
  • 10,828
  • 13
  • 69
  • 129
  • if paintCover returns false, it means it painted a template on the ImageView, and the thread runs to create the Bitmap and put it into the cache and paint it on the ImageView at a later time. If it returns true, it means the image was found on the cache, and put into the ImageView, so nothing is done then. However, after trying to find out I saw that the image on the cache was recycled, but I don't know who did it or how it happened. – dabicho Nov 04 '14 at 07:03
  • What do you want to do if the paintCover is true? – Blaze Tama Nov 04 '14 at 07:07
  • Thank you, I just found out what I did wrong... If the id's differ, I am wrongly recycling the bitmap because I already put it into the cache. My bad. But somehow watching/discusing the code here made it clear to me. If the paintCover is true, it means the cover has been painted with the final Bitmap (as it was found on the cache), so nothing at the moment. – dabicho Nov 04 '14 at 07:10
  • @dabicho i see. Great :) Just a suggestion, its a best practice to always specify "if" and "else" in the getView to avoid some bugs. You may need to do some googling to understand it better. – Blaze Tama Nov 04 '14 at 07:18