Search code examples
androidlistviewseekbar

SeekBar and ListView recycling issue


I am using this project in my new app which shows a list of tracks with a play button and a SeekBar progress. I am able to get the tracks playing but I got an issue with updating the SeekBar.

I use a Runnable and a Handler to update the SeekBar. When I hit play, the SeekBar of that child view, moving correctly, but the child at its recycled position got updated too. For example, I have a list of 10 items, with 5 visible items only. When 1st track's SeekBar got updating, the 6th track was also updated. The same thing happening with 11th or 16th... positions if I have bigger list. Any suggestion to how I could get it updated properly?

This is the adapter

public class ListViewAdapter extends BaseAdapter {
    private Activity activity;
    private List<Ring> list;
    private LayoutInflater inflater;
    private int mediaFileLengthInMilliseconds;
    private final Handler handler = new Handler();
    private MediaPlayer player;
    private AssetManager assetManager;

    public ListViewAdapter(Activity activity, List<Ring> list) {
        this.activity = activity;
        this.list = list;
        this.inflater = LayoutInflater.from(this.activity);
        assetManager = activity.getAssets();
        player = new MediaPlayer();
    }


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

    @Override
    public Ring getItem(int position) {
        return list.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }


    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {

        final ViewHolder holder;
        if (convertView == null) {
            holder = new ViewHolder();
            convertView = this.inflater.inflate(R.layout.list_view_item, parent, false);

            holder.play = (ImageButton) convertView.findViewById(R.id.play);
            holder.fav = (Button) convertView.findViewById(R.id.fav);
            holder.set = (Button) convertView.findViewById(R.id.setAs);
            holder.title = (TextView) convertView.findViewById(R.id.title);
            holder.seekBarProgress = (SeekBar) convertView.findViewById(R.id.bar);
            holder.seekBarProgress.setMax(99);
            holder.seekBarProgress.setEnabled(false);
            convertView.setTag(holder);

        }else{
            holder = (ViewHolder) convertView.getTag();
        }
        holder.title.setText(list.get(position).getName());
        holder.title.setTypeface(Typeface.createFromAsset(activity.getAssets(), "fonts/OpenSans-Light.ttf"));
        holder.play.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (player.isPlaying()) {
                    player.pause();
                    holder.play.setImageDrawable(ResourcesCompat.getDrawable(activity.getResources(), android.R.drawable.ic_media_play, null));
                    holder.primarySeekBarProgressUpdater();
                } else {
                    player.reset();
                    AssetFileDescriptor afd = null;
                    try {
                        afd = assetManager.openFd(list.get(position).getName() + ".mp3");
                        player.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
                        player.prepare();
                        player.start();
                        mediaFileLengthInMilliseconds = player.getDuration();
                        player.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {
                            @Override
                            public void onBufferingUpdate(MediaPlayer mp, int percent) {
                                holder.seekBarProgress.setSecondaryProgress(percent);
                            }
                        });
                        holder.seekBarProgress.setOnTouchListener(new View.OnTouchListener() {
                            @Override
                            public boolean onTouch(View v, MotionEvent event) {
                                if (v.getId() == R.id.bar) {
                                    if (player.isPlaying()) {
                                        SeekBar sb = (SeekBar) v;
                                        int playPositionInMillisecconds = (mediaFileLengthInMilliseconds / 100) * sb.getProgress();
                                        player.seekTo(playPositionInMillisecconds);
                                    }
                                }
                                return true;
                            }
                        });
                        player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
                            @Override
                            public void onCompletion(MediaPlayer mp) {
                                holder.play.setImageDrawable(ResourcesCompat.getDrawable(activity.getResources(), android.R.drawable.ic_media_play, null));
                            }
                        });
                        holder.primarySeekBarProgressUpdater();
                        holder.play.setImageDrawable(ResourcesCompat.getDrawable(activity.getResources(), android.R.drawable.ic_media_pause, null));
                    }catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        return convertView;
    }

    private class ViewHolder {
        TextView title;
        ImageButton play;
        Button set;
        Button fav;
        SeekBar seekBarProgress;

        void primarySeekBarProgressUpdater() {
            seekBarProgress.setProgress((int) (((float) player.getCurrentPosition() / mediaFileLengthInMilliseconds) * 100)); // This math construction give a percentage of "was playing"/"song length"
            if (player.isPlaying()) {
                Runnable notification = new Runnable() {
                    public void run() {
                        primarySeekBarProgressUpdater();
                    }
                };
                handler.postDelayed(notification,100);
            }
        }
    }
}

Solution

  • As you already pointed out, the list view items are recycled. As such, your Handler should be updating your backing data model with the value of the SeekBar and calling notifyDataSetChanged() on the adapter. The actual setting of the SeekBar value should be handled by your adapter's getView().

    This is not a perfect fix because there are a lot of problems with your code, but it should address your issue of multiple list items being updated.

    1. Add a position variable to your backing list model.
    2. Pass in the position of the item that you are updating into primarySeekBarProgressUpdater
    3. Call notifyDataSetChanged()
    4. In your getView, set the seekbar position based on your backing data model

    void primarySeekBarProgressUpdater(final int i) {
        list.get(i).setPosition((int) (((float) player.getCurrentPosition() / mediaFileLengthInMilliseconds) * 100)); // This math construction give a percentage of "was playing"/"song length"
        notifyDataSetChanged();
        if (player.isPlaying()) {
            Runnable notification = new Runnable() {
                public void run() {
                    primarySeekBarProgressUpdater(i);
                }
            };
            handler.postDelayed(notification,100);
        }
    }
    

    public View getView(final int position, View convertView, ViewGroup parent) {
        ...
        holder.seekBarProgress.setProgress(list.get(position).getPosition());
        ...
    }