Search code examples
androidandroid-listviewandroid-asynctaskandroid-scroll

Optimization of ListView with Asynctask for Smoother Scroll


I am working on a music application and a novice in android applications. I am using ListView to show content which is music files in the device. Here is what I am doing in the ListView:

  1. Showing music file title, artist and duration
  2. Album art of music file if available

So

The first point of what I am doing is easy to do and quite efficient with really smooth scroll behaviour, problem starts from second point i.e. loading album art of music file which I am doing with AsyncTask. I get AsyncTask working but it's not the smartest AsyncTask. For instance I want these two behaviours from my ListView and AsyncTask:

  1. Start doing AsyncTask only when user stops scrolling - I want to save important CPU cycles when user tries to fling through views (scrolls fastly all across). Right now with my approach, when fling occurs all AsyncTask starts loading right away. I have implemented function cancelPotentialTask() and it cancels the task right away if I dont need it but what if I can control starting the task then that would be more efficient than cancelling it. Like when user stop scrolling ListView I get a trigger to start AsyncTask.
  2. If AsyncTask once return result that no album art is available then it do not need to get into the task again if I call it again - This can be easily understood by picture below (sorry for poor editing). enter image description here

In first part, I let views to load up along with AsyncTask. In next one, I swipe up and get another view and let one row go to recycler. Third time I swipe down I get the recycled view back but Asynctask loads up but I do not need AsyncTask as it is really obvious result will be null as it was first time. I know code do not know this till we do not tell it. But how can I tell. I have a clue in getView() where I see if convertView == null.

Thanks for reading so far, I appreciate your patience. Here is the cheese for you -

   public Bitmap getAlbumart(Long album_id) {
        Bitmap bm = null;
        try {
            final Uri sArtworkUri = Uri
                    .parse("content://media/external/audio/albumart");

            Uri uri = ContentUris.withAppendedId(sArtworkUri, album_id);

            ParcelFileDescriptor pfd = activity.getContentResolver()
                    .openFileDescriptor(uri, "r");

            if (pfd != null) {
                FileDescriptor fd = pfd.getFileDescriptor();
                bm = BitmapFactory.decodeFileDescriptor(fd);
            }
        } catch (Exception ignored) {
        }
        return bm;
    }

    // This is what I call to get image in getView()
    public void loadBitmap(String resId, ImageView imageView) {
        if (cancelPotentialWork(resId, imageView)) {
            String imageKey = String.valueOf(resId);
            LoadRows task = new LoadRows(imageView);
            SoftReference<Bitmap> bitmap1;
            try {
                bitmap1 = new SoftReference<>(Bitmap.createScaledBitmap(getBitmapFromMemCache(imageKey), 80, 80, true));
                imageView.setImageBitmap(bitmap1.get());
            } catch (Exception e) {
                SoftReference<AsyncDrawable> asyncDrawable = new SoftReference<>(new AsyncDrawable(resId, null,
                        task));
                imageView.setImageDrawable(asyncDrawable.get());
                task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, resId);
            }
        }
    }

    public class LoadRows extends AsyncTask<String, Void, Bitmap> {

        String data = null;
        private final WeakReference<ImageView> imageViewReference;

        public LoadRows(ImageView imageView) {
            // Use a WeakReference to ensure the ImageView can be garbage
            // collected
            imageViewReference = new WeakReference<ImageView>(imageView);
        }

        @Override
        protected Bitmap doInBackground(String... params) {
            data = params[0];

            Bitmap imageResult;
            imageResult = getAlbumart(Long.parseLong(data));

            if (imageResult != null) {
                imageResult = Bitmap.createScaledBitmap(imageResult, 300, 300, true);
                addBitmapToMemoryCache(data, imageResult);
                imageResult = Bitmap.createScaledBitmap(imageResult, 80, 80, true);
            }
            return imageResult;
        }


        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);

            if (isCancelled()) {
                bitmap = null;
            }

            if (bitmap != null) {
                ImageView imageView = imageViewReference.get();
                LoadRows bitmapWorkerTask = getLoadRows(imageView);
                if (this == bitmapWorkerTask) {
                    imageView.setImageBitmap(bitmap);
                }
            }

        }

    }



    static class AsyncDrawable extends BitmapDrawable {
        private final WeakReference<LoadRows> LoadRowsReference;

        @SuppressWarnings("deprecation")
        public AsyncDrawable(String res, Bitmap bitmap, LoadRows LoadRows) {
            super(bitmap);
            LoadRowsReference = new WeakReference<LoadRows>(LoadRows);
        }

        public LoadRows getLoadRows() {
            return LoadRowsReference.get();
        }
    }

    public static boolean cancelPotentialWork(String data, ImageView imageView) {
        final LoadRows LoadRows = getLoadRows(imageView);

        if (LoadRows != null) {
            final String bitmapData = LoadRows.data;
            // If bitmapData is not yet set or it differs from the new data
            if (bitmapData == "0" || bitmapData != data) {
                // Cancel previous task
                LoadRows.cancel(true);
            } else {
                // The same work is already in progress
                return false;
            }
        }
        // No task associated with the ImageView, or an existing task was
        // cancelled
        return true;
    }

    private static LoadRows getLoadRows(ImageView imageView) {
        if (imageView != null) {
            final Drawable drawable = imageView.getDrawable();
            if (drawable instanceof AsyncDrawable) {
                final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
                return asyncDrawable.getLoadRows();
            }
        }

        return null;
    }

Please guys take your time, I have no hurry to accept any answer really soon. Hope someone can play with code or if nothing but explain me what should be my approach.

If you guys need anything else to know, just let me know. Thank you!

Edit

getView() method -

@SuppressWarnings("deprecation")
    @SuppressLint("SimpleDateFormat")
    public View getView(final int position, View convertView, ViewGroup parent) {
        View vi = convertView;
        final ViewHolder holder;

        if (convertView == null) {

            /****** Inflate tabitem.xml file for each row ( Defined below ) *******/
            vi = inflater.inflate(R.layout.row, null);

            /****** View Holder Object to contain tabitem.xml file elements ******/

            holder = new ViewHolder();
            holder.text = (TextView) vi.findViewById(R.id.text);
            holder.text1 = (TextView) vi.findViewById(R.id.text1);
            holder.duration = (TextView) vi.findViewById(R.id.textTime);
            holder.image = (ImageView) vi.findViewById(R.id.image);
            holder.btn = (Button) vi.findViewById(R.id.button1);

            /************ Set holder with LayoutInflater ************/
            vi.setTag(holder);

        } else {
            holder = (ViewHolder) vi.getTag();
        }


        if (data.size() > 0) {

            /***** Get each Model object from Arraylist ********/
            tempValues = null;
            tempValues = (ListModel) data.get(position);
            final String duration = tempValues.getDuration();
            final String artist = tempValues.getArtist();
            final String songName = tempValues.getName();
            final String title = tempValues.getTitle();
            final String albumid = tempValues.getAlbumID();

            String finalTitle;
            if (title != null) {
                finalTitle = title;
            } else {
                finalTitle = songName;
            }
            loadBitmap(albumid, holder.image); // HERE I GET IMAGE ***********

            holder.text.setText(finalTitle);
            holder.text1.setText(artist);
            holder.duration.setText(duration);

            vi.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    MusicUtils.play(position, listModelToArrayList(), activity, songsManager);
                }
            });
            final PopupMenu pop = new PopupMenu(activity, holder.btn);
            int[] j = new int[6];
            j[0] = MusicUtils.PLAY;
            j[1] = MusicUtils.PLAY_NEXT;
            j[2] = MusicUtils.ADD_TO_QUEUE;
            j[3] = MusicUtils.ADD_TO_PLAYLIST;
            j[4] = MusicUtils.SHUFFLE_PLAY;
            j[5] = MusicUtils.DELETE;
            MusicUtils.generateMenu(pop, j);
            pop.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
                public boolean onMenuItemClick(MenuItem item) {
                    switch (item.getItemId()) {
                        case MusicUtils.PLAY:
                            MusicUtils.play(position, listModelToArrayList(), activity, songsManager);
                            return true;
                        case MusicUtils.DELETE:
                            return true;
                        case MusicUtils.PLAY_NEXT:
                            MusicUtils.playNext(listModelToArrayList().get(position));
                            return true;
                        case MusicUtils.ADD_TO_QUEUE:
                            MusicUtils.addToQueue(listModelToArrayList().get(position), songsManager);
                            return true;
                        case MusicUtils.ADD_TO_PLAYLIST:
                            MusicUtils.addToPlaylist(position, listModelToArrayList().get(position), activity, songsManager);
                            return true;
                        case MusicUtils.SHUFFLE_PLAY:
                            MusicUtils.shufflePlay(position, listModelToArrayList(), activity, songsManager);
                            return true;
                        default:
                            return false;
                    }
                }
            });


            holder.btn.setOnClickListener(new View.OnClickListener() {

                @Override
                public void onClick(View v) {
                    pop.show();
                }
            });

            tempValues = null;
            vi.setVisibility(View.VISIBLE);
        } else {
            vi.setVisibility(View.INVISIBLE);
        }
        return vi;
    }

Update

With using UIL, the result is almost same as the code I provided up i.e. laggy. But Picasso is really worth trying I would say the lag is almost gone. This is almost what I was looking to implement.


Solution

  • The laggy scroll is gone with usage of library Picasso. I am still not sure what can be exact reason for this but just replacing my AsyncTask with Picasso works like charm. It is much more smoother and efficient as caching is done by library itself.

    Here is the library https://github.com/square/picasso.