Android: How to speed up image loading in GridView even after using ViewHolder

978 Views Asked by At

Well, I'm developing a Gallery App for android. However, when I scroll down, the images still take a while to load. Why is that? I'm already loading a scaled down version of the images and using ViewHolder as well. Below is the code for the MainActivity.java. Any help would be highly appreciated. Thanks. PS: Should I use libraries like Universal Image Loader? Will that make any difference to the problem that I'm facing currently?

package com.example.om.imageviewer3;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;

import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.Toast;

public class MainActivity extends Activity {
    String[]imgLoc;

    AsyncTaskLoadFiles myAsyncTaskLoadFiles;

    public class AsyncTaskLoadFiles extends AsyncTask<Void, String, Void> {

        File targetDirector;
        ImageAdapter myTaskAdapter;

        public AsyncTaskLoadFiles(ImageAdapter adapter) {
            myTaskAdapter = adapter;
        }

        @Override
        protected void onPreExecute() {
            String ExternalStorageDirectoryPath = Environment
                    .getExternalStorageDirectory().getAbsolutePath();

            String targetPath = ExternalStorageDirectoryPath + "/DCIM/Camera/";
            targetDirector = new File(targetPath);
            myTaskAdapter.clear();

            super.onPreExecute();
        }

        @Override
        protected Void doInBackground(Void... params) {

            File[] files = targetDirector.listFiles();
            Arrays.sort(files);
            for (File file : files) {
                publishProgress(file.getAbsolutePath());
                if (isCancelled()) break;
            }
            int i=0;
            for(File file:files){
                i++;
            }
            int len=i;
            imgLoc=new String[len];
            i=0;
            for(File file:files){
                imgLoc[i]=file.getAbsolutePath();
                i++;
            }

            return null;
        }

        @Override
        protected void onProgressUpdate(String... values) {
            myTaskAdapter.add(values[0]);
            super.onProgressUpdate(values);
        }

        @Override
        protected void onPostExecute(Void result) {
            myTaskAdapter.notifyDataSetChanged();
            super.onPostExecute(result);
        }

    }

    public class ImageAdapter extends BaseAdapter {

        private Context mContext;
        ArrayList<String> itemList = new ArrayList<String>();

        public ImageAdapter(Context c) {
            mContext = c;
        }

        void add(String path) {
            itemList.add(path);
        }

        void clear() {
            itemList.clear();
        }

        void remove(int index){
            itemList.remove(index);
        }

        @Override
        public int getCount() {
            return itemList.size();
        }

        @Override
        public Object getItem(int position) {
            // TODO Auto-generated method stub
            return itemList.get(position);
        }

        @Override
        public long getItemId(int position) {
            // TODO Auto-generated method stub
            return 0;
        }

        //getView load bitmap ui thread
  /*
  @Override
  public View getView(int position, View convertView, ViewGroup parent) {
   ImageView imageView;
   if (convertView == null) { // if it's not recycled, initialize some
          // attributes
    imageView = new ImageView(mContext);
    imageView.setLayoutParams(new GridView.LayoutParams(220, 220));
    imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
    imageView.setPadding(8, 8, 8, 8);
   } else {
    imageView = (ImageView) convertView;
   }

   Bitmap bm = decodeSampledBitmapFromUri(itemList.get(position), 220,
     220);

   imageView.setImageBitmap(bm);
   return imageView;
  }
  */

        //getView load bitmap in AsyncTask
        @Override
        public View getView(final int position, View convertView, ViewGroup parent) {


            ImageView imageView;
            if (convertView == null) { // if it's not recycled, initialize some
                // attributes
                imageView = new ImageView(mContext);
                imageView.setLayoutParams(new GridView.LayoutParams(500,250));
                imageView.setScaleType(ImageView.ScaleType.FIT_CENTER);
                imageView.setPadding(20,0,20,0);
                ViewHolder dataHolder=new ViewHolder();
                dataHolder.image=imageView;
                imageView.setTag(dataHolder);

                convertView = imageView;

            }
                ViewHolder holder=(ViewHolder)convertView.getTag();
                holder.image.setImageBitmap(null);
                holder.position=position;

                //imageView = (ImageView) convertView;



            //Bitmap bm = decodeSampledBitmapFromUri(itemList.get(position), 220, 220);
            // Using an AsyncTask to load the slow images in a background thread
            new AsyncTask<ViewHolder, Void, Bitmap>() {
                private ViewHolder v;

                @Override
                protected Bitmap doInBackground(ViewHolder... params) {
                    v = params[0];
                    Bitmap bm = decodeSampledBitmapFromUri(itemList.get(position), 500, 250);
                    return bm;
                }

                @Override
                protected void onPostExecute(Bitmap result) {
                    super.onPostExecute(result);
                    //Not work for me!

           if (v.position == position) {
               // If this item hasn't been recycled already,
            // show the image
               v.image.setImageBitmap(result);
           }


                   //v.image.setImageBitmap(result);

                }
            }.execute(holder);

            //imageView.setImageBitmap(bm);
            //return imageView;
            return convertView;
        }

        public Bitmap decodeSampledBitmapFromUri(String path, int reqWidth,
                                                 int reqHeight) {

            Bitmap bm = null;
            // First decode with inJustDecodeBounds=true to check dimensions
            final BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true;
            BitmapFactory.decodeFile(path, options);

            // Calculate inSampleSize
            options.inSampleSize = calculateInSampleSize(options, reqWidth,
                    reqHeight);

            // Decode bitmap with inSampleSize set
            options.inJustDecodeBounds = false;
            bm = BitmapFactory.decodeFile(path, options);

            return bm;
        }

        public 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 = 1;

            if (height > reqHeight || width > reqWidth) {
                if (width > height) {
                    inSampleSize = Math.round((float) height
                            / (float) reqHeight);
                } else {
                    inSampleSize = Math.round((float) width / (float) reqWidth);
                }
            }

            return inSampleSize;
        }

        class ViewHolder {
            ImageView image;
            int position;
        }

    }

    ImageAdapter myImageAdapter;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final GridView gridview = (GridView) findViewById(R.id.gridview);
        myImageAdapter = new ImageAdapter(this);
        gridview.setAdapter(myImageAdapter);
        //grid.seto

  /*
   * Move to asyncTaskLoadFiles String ExternalStorageDirectoryPath =
   * Environment .getExternalStorageDirectory() .getAbsolutePath();
   *
   * String targetPath = ExternalStorageDirectoryPath + "/test/";
   *
   * Toast.makeText(getApplicationContext(), targetPath,
   * Toast.LENGTH_LONG).show(); File targetDirector = new
   * File(targetPath);
   *
   * File[] files = targetDirector.listFiles(); for (File file : files){
   * myImageAdapter.add(file.getAbsolutePath()); }
   */
        myAsyncTaskLoadFiles = new AsyncTaskLoadFiles(myImageAdapter);
        myAsyncTaskLoadFiles.execute();

        //gridview.setOnItemClickListener(myOnItemClickListener);

        /*Button buttonReload = (Button)findViewById(R.id.reload);
        buttonReload.setOnClickListener(new OnClickListener(){

            @Override
            public void onClick(View arg0) {

                //Cancel the previous running task, if exist.
                myAsyncTaskLoadFiles.cancel(true);

                //new another ImageAdapter, to prevent the adapter have
                //mixed files
                myImageAdapter = new ImageAdapter(MainActivity.this);
                gridview.setAdapter(myImageAdapter);
                myAsyncTaskLoadFiles = new AsyncTaskLoadFiles(myImageAdapter);
                myAsyncTaskLoadFiles.execute();
            }});*/
        gridview.setOnItemClickListener(new OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                seeImage(view,position);


            }
        });

    }
    public void seeImage(View v, int position){
        Intent intent=new Intent(this.getApplicationContext(), SeeImage.class);
        intent.putExtra("getimgloc",""+imgLoc[position]);
        startActivity(intent);
    }
}

Edit: Ok, so here's the code after the update(As helped by @Raiv)

package com.example.om.imageviewer3;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;

import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.LruCache;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.Toast;

public class MainActivity extends Activity {
    String[]imgLoc;
    private LruCache<String,Bitmap> mMemoryCache;


    AsyncTaskLoadFiles myAsyncTaskLoadFiles;

    public class AsyncTaskLoadFiles extends AsyncTask<Void, String, Void> {

        File targetDirector;
        ImageAdapter myTaskAdapter;

        public AsyncTaskLoadFiles(ImageAdapter adapter) {
            myTaskAdapter = adapter;
        }

        @Override
        protected void onPreExecute() {
            String ExternalStorageDirectoryPath = Environment
                    .getExternalStorageDirectory().getAbsolutePath();

            String targetPath = ExternalStorageDirectoryPath + "/DCIM/Camera/";
            targetDirector = new File(targetPath);
            myTaskAdapter.clear();

            super.onPreExecute();
        }

        @Override
        protected Void doInBackground(Void... params) {

            File[] files = targetDirector.listFiles();
            Arrays.sort(files);
            for (File file : files) {
                publishProgress(file.getAbsolutePath());
                if (isCancelled()) break;
            }
            int i=0;
            for(File file:files){
                i++;
            }
            int len=i;
            imgLoc=new String[len];
            i=0;
            for(File file:files){
                imgLoc[i]=file.getAbsolutePath();
                i++;
            }

            return null;
        }

        @Override
        protected void onProgressUpdate(String... values) {
            myTaskAdapter.add(values[0]);
            super.onProgressUpdate(values);
        }

        @Override
        protected void onPostExecute(Void result) {
            myTaskAdapter.notifyDataSetChanged();
            super.onPostExecute(result);
        }

    }

    public class ImageAdapter extends BaseAdapter {

        private Context mContext;
        ArrayList<String> itemList = new ArrayList<String>();

        public ImageAdapter(Context c) {
            mContext = c;
        }

        void add(String path) {
            itemList.add(path);
        }

        void clear() {
            itemList.clear();
        }

        void remove(int index){
            itemList.remove(index);
        }

        @Override
        public int getCount() {
            return itemList.size();
        }

        @Override
        public Object getItem(int position) {
            // TODO Auto-generated method stub
            return itemList.get(position);
        }

        @Override
        public long getItemId(int position) {
            // TODO Auto-generated method stub
            return 0;
        }

        //getView load bitmap ui thread
  /*
  @Override
  public View getView(int position, View convertView, ViewGroup parent) {
   ImageView imageView;
   if (convertView == null) { // if it's not recycled, initialize some
          // attributes
    imageView = new ImageView(mContext);
    imageView.setLayoutParams(new GridView.LayoutParams(220, 220));
    imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
    imageView.setPadding(8, 8, 8, 8);
   } else {
    imageView = (ImageView) convertView;
   }

   Bitmap bm = decodeSampledBitmapFromUri(itemList.get(position), 220,
     220);

   imageView.setImageBitmap(bm);
   return imageView;
  }
  */

        //getView load bitmap in AsyncTask
        @Override
        public View getView(final int position, View convertView, ViewGroup parent) {


            ImageView imageView;
            if (convertView == null) { // if it's not recycled, initialize some
                // attributes
                imageView = new ImageView(mContext);
                imageView.setLayoutParams(new GridView.LayoutParams(220,220));
                imageView.setScaleType(ImageView.ScaleType.FIT_CENTER);
                imageView.setPadding(20,0,20,0);
                ViewHolder dataHolder=new ViewHolder();
                dataHolder.image=imageView;
                imageView.setTag(dataHolder);

                convertView = imageView;

            }
                ViewHolder holder=(ViewHolder)convertView.getTag();
                holder.image.setImageBitmap(null);
                holder.position=position;

                //imageView = (ImageView) convertView;



            //Bitmap bm = decodeSampledBitmapFromUri(itemList.get(position), 220, 220);
            // Using an AsyncTask to load the slow images in a background thread
            new AsyncTask<ViewHolder, Void, Bitmap>() {
                private ViewHolder v;

                @Override
                protected Bitmap doInBackground(ViewHolder... params) {
                    v = params[0];
                    //String a=itemList.
                    String hash= getMD5Hash(itemList.get(position))+".jpg";
                    Bitmap bm=null;
                    synchronized (mMemoryCache){
                        bm=mMemoryCache.get(hash);
                    }
                    if(bm==null){
                        File cacheFile=new File(Environment.getDownloadCacheDirectory(),hash);
                        if(cacheFile.exists()){
                            bm=BitmapFactory.decodeFile(cacheFile.getAbsolutePath());
                            if(bm!=null){
                                synchronized (mMemoryCache){
                                    bm=mMemoryCache.put(hash,bm);
                                }
                                return bm;
                            }
                        }
                    }else{
                        return bm;
                    }
                    if(bm==null){
                        bm=decodeSampledBitmapFromUri(itemList.get(position),220,250);
                        FileOutputStream out = null;
                        try {
                            out = new FileOutputStream(Environment.getDownloadCacheDirectory());
                            bm.compress(Bitmap.CompressFormat.PNG, 100, out); // bmp is your Bitmap instance
                            // PNG is a lossless format, the compression factor (100) is ignored
                        } catch (Exception e) {
                            e.printStackTrace();
                        } finally {
                            try {
                                if (out != null) {
                                    out.close();
                                }
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }


                    }
                    return bm;



                }
                public String getMD5Hash(String a){//throws NoSuchAlgorithmException{
                    try {
                        MessageDigest m = MessageDigest.getInstance("MD5");
                        m.reset();
                        m.update(a.getBytes());
                        return new BigInteger(1, m.digest()).toString(16);
                    }catch(NoSuchAlgorithmException e){}
                    return "Error";

                }

                @Override
                protected void onPostExecute(Bitmap result) {
                    super.onPostExecute(result);
                    //Not work for me!

           if (v.position == position) {
               // If this item hasn't been recycled already,
            // show the image
               v.image.setImageBitmap(result);
           }


                   //v.image.setImageBitmap(result);

                }
            }.execute(holder);

            //imageView.setImageBitmap(bm);
            //return imageView;
            return convertView;
        }

        public Bitmap decodeSampledBitmapFromUri(String path, int reqWidth,
                                                 int reqHeight) {

            Bitmap bm = null;
            // First decode with inJustDecodeBounds=true to check dimensions
            final BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true;
            BitmapFactory.decodeFile(path, options);

            // Calculate inSampleSize
            options.inSampleSize = calculateInSampleSize(options, reqWidth,
                    reqHeight);

            // Decode bitmap with inSampleSize set
            options.inJustDecodeBounds = false;
            bm = BitmapFactory.decodeFile(path, options);

            return bm;
        }

        public 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 = 1;

            if (height > reqHeight || width > reqWidth) {
                if (width > height) {
                    inSampleSize = Math.round((float) height
                            / (float) reqHeight);
                } else {
                    inSampleSize = Math.round((float) width / (float) reqWidth);
                }
            }

            return inSampleSize;
        }

        class ViewHolder {
            ImageView image;
            int position;
        }

    }

    ImageAdapter myImageAdapter;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final GridView gridview = (GridView) findViewById(R.id.gridview);
        myImageAdapter = new ImageAdapter(this);
        gridview.setAdapter(myImageAdapter);
        //grid.seto

  /*
   * Move to asyncTaskLoadFiles String ExternalStorageDirectoryPath =
   * Environment .getExternalStorageDirectory() .getAbsolutePath();
   *
   * String targetPath = ExternalStorageDirectoryPath + "/test/";
   *
   * Toast.makeText(getApplicationContext(), targetPath,
   * Toast.LENGTH_LONG).show(); File targetDirector = new
   * File(targetPath);
   *
   * File[] files = targetDirector.listFiles(); for (File file : files){
   * myImageAdapter.add(file.getAbsolutePath()); }
   */
        myAsyncTaskLoadFiles = new AsyncTaskLoadFiles(myImageAdapter);
        myAsyncTaskLoadFiles.execute();

        //gridview.setOnItemClickListener(myOnItemClickListener);

        /*Button buttonReload = (Button)findViewById(R.id.reload);
        buttonReload.setOnClickListener(new OnClickListener(){

            @Override
            public void onClick(View arg0) {

                //Cancel the previous running task, if exist.
                myAsyncTaskLoadFiles.cancel(true);

                //new another ImageAdapter, to prevent the adapter have
                //mixed files
                myImageAdapter = new ImageAdapter(MainActivity.this);
                gridview.setAdapter(myImageAdapter);
                myAsyncTaskLoadFiles = new AsyncTaskLoadFiles(myImageAdapter);
                myAsyncTaskLoadFiles.execute();
            }});*/
        gridview.setOnItemClickListener(new OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                seeImage(view,position);


            }
        });
        final int maxMemory=(int)(Runtime.getRuntime().maxMemory()/1024);
        final int cacheSize=maxMemory/8;
        mMemoryCache=new LruCache<String, Bitmap>(cacheSize){
            @Override
        protected int sizeOf(String key,Bitmap bitmap){
                return bitmap.getByteCount()/1024;
            }
        };



    }
    public void seeImage(View v, int position){
        Intent intent=new Intent(this.getApplicationContext(), SeeImage.class);
        intent.putExtra("getimgloc",""+imgLoc[position]);
        startActivity(intent);
    }
}
1

There are 1 best solutions below

6
On

You should use some image caching for that. First - don't reload your images every time, save them on disk once loaded. for example: a) create hash for every link you use, and save images by hash in cache folder. b) on loading, check by hash for file existance and if it exists use cached file.Also you may add in-memory caching for further speed-up.

   new AsyncTask<ViewHolder, Void, Bitmap>() {
        private ViewHolder v;

        @Override
        protected Bitmap doInBackground(ViewHolder... params) {
            v = params[0];
            String hash = Utils.getMD5Hash(itemList.get(position))+".jpg"// implement hash yourself

            Bitmap bm = null;
            synchronized (mMemoryCache) {
               bm = mMemoryCache.get(hash);
               return bm;
            }
            File cacheFile = new File( Environment.getDownloadCacheDirectory(),hash);
            if(bm==null)
            {

               if(cacheFile.exists()){
                   bm = /*try to load from cache dir*/
                   if(bm!=null){
                       synchronized (mMemoryCache) {
                           bm = mMemoryCache.put(hash,bm);
                       }
                       return bm;
                   }
               }
            }else
            {
               return bm;
            }
           if(bm==null)
            {
              bm =decodeSampledBitmapFromUri(itemList.get(position), 500, 250);
              /*save on disk*/
              /*save in mem cache*/
            return bm;
            }
        }

        @Override
        protected void onPostExecute(Bitmap result) {
            super.onPostExecute(result);
            //Not work for me!

   if (v.position == position) {
       // If this item hasn't been recycled already,
    // show the image
       v.image.setImageBitmap(result);
   }


           //v.image.setImageBitmap(result);

        }
    }.execute(holder);

in-memory cache implementation below, look for docs here

private LruCache<String, Bitmap> mMemoryCache;

...

    // Get max available VM memory, exceeding this amount will throw an
    // OutOfMemory exception. Stored in kilobytes as LruCache takes an
    // int in its constructor.
    final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

    // Use 1/8th of the available memory for this memory cache.
    final int cacheSize = maxMemory / 8;

    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap bitmap) {
            // The cache size will be measured in kilobytes rather than
            // number of items.
            return bitmap.getByteCount() / 1024;
        }
    };

md5 hash implementation:

    String getMD5(String data){
        MessageDigest m = MessageDigest.getInstance("MD5");
        m.reset();
        m.update(data.getBytes());
        return new BigInteger(1, m.digest()).toString(16);
    }

load bitmap bm = BitmapFactory.decodeFile(cacheFile.getAbsolutePath());

save bitmap here