ListView recycling switches seekbar position

2019-07-11 18:24发布

I have a strange problem. I have a custom ListView with BaseAdapter. In my ListView row layout, I have few TextViews, Buttons, and a SeekBar. Everything works great, no problems with anything except with recycling.

What happens: All SeekBar's are hidden, except one which is visible. The SeekBar is visible when MediaPlayer on row is playing. That part is working great too.

But, when user scrolls up or down, and the row with visible SeekBar is out of view, it recycles, and SeekBar is recycled too and it keeps updating even tho it's not visible and that row is not playing(mp). And when users scrolls back to return to the view which is playing, SeekBar is visible, but not updating and it's position is 0. Instead random SeekBar is being updated, but it's not visible(I did test when all SeekBar's are visible so I know that happens)

Of course I could've done most idiotic solution and disable ListView recycling but this makes user experience really bad and could possibly make app run out of memory, and using LargeHeap is lame. So the following methods are out of question.

@Override
public int getViewTypeCount() {
    return getCount();
}

@Override
public int getItemViewType(int position) {
    return position;
}

My question: Is there any obvious solution to this? How do I keep only one row from being recycled? I won't post code until it's really necessary, the code is too large. Instead I will just post important parts related to the issue:

@Override
public View getView(final int position, View convertView, ViewGroup parent) {
    View row = convertView;
    holder = null;
    if (row == null) {
        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        row = inflater.inflate(R.layout.item, parent, false);
        holder = new Ids(row);
        row.setTag(holder);
    } else {
        holder = (Ids) row.getTag();
    }

    rowItemClass = (ListViewRow) getItem(position);
    if (Globals.isPlaying && Globals.pos == position) {        
        holder.seekbar.setVisibility(View.VISIBLE);
    } else {
        holder.seekbar.setVisibility(View.GONE);    
    }

    holder.seekbar.setTag(position);
    final Ids finalHolder = holder;

    ...
    OnClickListener{....
    Globals.mp.start();
    finalHolder.seekbar.setProgress(0);
    finalHolder.seekbar.setMax(Globals.mp.getDuration());
    ..

    updateTimeProgressBar = new Runnable() {
        @Override
        public void run() {
            if (Globals.isPlaying) {  
                finalHolder.seekbar.setProgress(Globals.mp.getCurrentPosition());
                int currentPosition = Globals.mp.getCurrentPosition();
                handler.postDelayed(this, 100);
            }
        }
    };

    handler.postDelayed(updateTimeProgressBar, 100);
    finalHolder.seekbar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
        @Override
        public void onStopTrackingTouch(SeekBar seekBar) {
            handler.removeCallbacks(updateTimeProgressBar);
            Globals.mp.seekTo(finalHolder.seekbar.getProgress());
            int currentPosition = Globals.mp.getCurrentPosition();
            handler.postDelayed(updateTimeProgressBar, 500);
        }

        @Override
        public void onStartTrackingTouch(SeekBar seekBar) {
            handler.removeCallbacks(updateTimeProgressBar);
        }

        @Override
        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
        // Intentionally left empty
        }
    });
}

and That's it. Why is this happening? Is there a way to disable recycling for one row? Would be it be a bad idea to update all SeekBars in ListView since only 1 mp will be playing? How bad is that for performance?

1条回答
啃猪蹄的小仙女
2楼-- · 2019-07-11 19:10

I'll outline one of the best ways to tackle this (in my opinion, of course) while respecting a ListView's ability to recycle.

But first, this has got to go:

@Override
public int getViewTypeCount() {
    return getCount();
}

@Override
public int getItemViewType(int position) {
    return position;
}

Solution:

Subclass SeekBar and let it handle all updates:

  • Define two methods that will mark this SeekBar active & inactive.

  • When marking a SeekBar active, provide the current position (progress). Alongside, post the Runnable that will update this SeekBar

  • When marking a SeekBar inactive, simply remove callbacks for the Runnable and call it a day

Inside your Adapter, the only state that you need to maintain is keeping track of the currently playing song. You can do this either by tracking the position as seen by your ListView, or by holding on to the ID of the currently playing track (this, of course, will only work if your model uses IDs).

Something like this:

// Define `setActiveAtPosition(int position)` and `setInactive()` 
// in your custom SeekBar

if (Globals.isPlaying && Globals.pos == position) {   
    // Define `setActiveAtPosition(int position)` in your custom SeekBar     
    holder.seekbar.setActiveAtPosition(Globals.mp.getCurrentPosition());
    holder.seekbar.setVisibility(View.VISIBLE);
} else {
    holder.seekbar.setInactiveForNow();
    holder.seekbar.setVisibility(View.GONE);    
}

Requested code:

Custom Seekbar class implementation:

public class SelfUpdatingSeekbar extends SeekBar implements SeekBar.OnSeekBarChangeListener {

    private boolean mActive = false;

    private Runnable mUpdateTimeProgressBar = new Runnable() {
        @Override
        public void run() {
            if (Globals.isPlaying) {
                setProgress(Globals.mp.getCurrentPosition());
                postDelayed(this, 100L);
            }
        }
    };

    public SelfUpdatingSeekbar(Context context, AttributeSet attrs) {
        super(context, attrs);
        setOnSeekBarChangeListener(this);
    }

    public void setActive() {
        if (!mActive) {
            mActive = true;
            removeCallbacks(mUpdateTimeProgressBar);
            setProgress(Globals.mp.getCurrentPosition());
            setVisibility(View.VISIBLE);
            postDelayed(mUpdateTimeProgressBar, 100L);
        }
    }

    public void setInactive() {
        if (mActive) {
            mActive = false;
            removeCallbacks(mUpdateTimeProgressBar);
            setVisibility(View.INVISIBLE);
        }
    }

    @Override
    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {

    }

    @Override
    public void onStartTrackingTouch(SeekBar seekBar) {
        removeCallbacks(mUpdateTimeProgressBar);
    }

    @Override
    public void onStopTrackingTouch(SeekBar seekBar) {
        removeCallbacks(mUpdateTimeProgressBar);
        Globals.mp.seekTo(getProgress());
        postDelayed(mUpdateTimeProgressBar, 500);
    }
}

In your adapter's getView(...):

if (Globals.isPlaying && Globals.pos == position) {        
    holder.seekbar.setActive();
} else {
    holder.seekbar.setInactive();    
}

Needless to say that you will need to use this custom implementation in your xml layout:

<com.your.package.SelfUpdatingSeekbar
    ....
    .... />
查看更多
登录 后发表回答