简体   繁体   English

RecyclerView滚动时随机更改位置

[英]RecyclerView changing position randomly while scrolling

I have made a chat app where I have used a RecyclerView . 我制作了一个聊天应用程序,在其中使用了RecyclerView Messages can be either text messages or audio messages. 消息可以是文本消息或音频消息。 Everything is working fine except when I change the timer text on the TextView (for audio player layout I have made) , of how long has the song been played. 一切正常,除非更改了TextView上的计时器文本(对于我制作的音频播放器布局),即歌曲已播放多长时间。 I do this in a Runnable . 我在Runnable执行此操作。 But when I scroll the RecyclerView , the timer TextView changes text at random position. 但是,当我滚动RecyclerView ,计时器TextView会在随机位置更改文本。

Here is how I am changing the TextView text: 这是我更改TextView文本的方式:

public void updateTimer(final int position) {
    View view = mRecyclerViewChat.getLayoutManager().findViewByPosition(position);
    timer = (TextView) view.findViewById(R.id.timer);

     r = new Runnable() {
        public void run() {
            int currentDuration;
            if (player.isPlaying()) {
                currentDuration = player.getCurrentPosition();
                timer.setText("" + milliSecondsToTimer((long) currentDuration));
                timer.postDelayed(this, 1000);
            } else {
                timer.removeCallbacks(this);
            }
        }
    };

    timer.post(r);
}

Here position is the position value I am getting from the onBindViewHolder . 这里position是我从onBindViewHolder获得的位置值。

EDIT 编辑

Here is the onBindViewHolder 这是onBindViewHolder

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    if (TextUtils.equals(mChats.get(position).senderUid,
            FirebaseAuth.getInstance().getCurrentUser().getUid())) {
        if (mChats.get(position).mediaUrlLocal == null) {
            configureMyChatViewHolder((MyChatViewHolder) holder, position);
        } else {
            configureMyChatMediaViewHolder((MyChatMediaViewHolder) holder, position);
        }
    } else {
        if (mChats.get(position).mediaUrlLocal == null) {
            configureOtherChatViewHolder((OtherChatViewHolder) holder, position);
        } else {
            configureOtherChatMediaViewHolder((OtherChatMediaViewHolder) holder, position);
        }
    }
}

and here is the playMedia method which is called from configureMyChatMediaViewHolder method: 这是从configureMyChatMediaViewHolder方法调用的playMedia方法:

private void playMyMedia(final MyChatMediaViewHolder myChatViewHolder, final Chat chat, final int position) {
    MediaMetadataRetriever metaRetriever = new MediaMetadataRetriever();
    metaRetriever.setDataSource(chat.mediaUrlLocal);

    String duration =
            metaRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
    long dur = Long.parseLong(duration);
    String seconds = String.valueOf((dur % 60000) / 1000);

    String minutes = String.valueOf(dur / 60000);
    String out = minutes + ":" + seconds;

    myChatViewHolder.timer.setText(out);

    if (chat.isPlay) {
        myChatViewHolder.play.setVisibility(View.GONE);
        myChatViewHolder.pause.setVisibility(View.VISIBLE);
    } else {
        myChatViewHolder.play.setVisibility(View.VISIBLE);
        myChatViewHolder.pause.setVisibility(View.GONE);
    }

    myChatViewHolder.play.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            chat.isPlay = !chat.isPlay;
            if (previousChat != position && previousChat != -1) {
                previousChatObj = mChats.get(previousChat);
            }
            previousChat = position;
            myChatViewHolder.play.setVisibility(View.GONE);
            myChatViewHolder.pause.setVisibility(View.VISIBLE);
            callback.onPlayClickListener(chat, previousChatObj, position);
        }
    });

    myChatViewHolder.pause.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            chat.isPlay = !chat.isPlay;
            myChatViewHolder.play.setVisibility(View.VISIBLE);
            myChatViewHolder.pause.setVisibility(View.GONE);
            callback.onPauseClickListener(chat, position);
        }
    });
}

I have only one media player instance in the fragment from which the adapter for chat is set. 片段中只有一个媒体播放器实例,从该实例可以设置聊天适配器。

Recycler view is re-using the views that are not currently visible. 回收者视图正在重新使用当前不可见的视图。 So if your audio player starts updating one Text View , when you scroll that textview is the same reference but with different text ( song ), but your runnable instance is still active and doing its job to update the Text View with same reference. 因此,如果您的音频播放器开始更新一个Text View,则在滚动时该TextView是相同的引用,但具有不同的文本(Song),但是您的可运行实例仍处于活动状态,并正在使用相同的引用来更新Text View。

  1. You should create a custom Runnable class which will hold a position value and will be only responsible for the TextView it should update. 您应该创建一个自定义Runnable类,该类将保存一个位置值,并且仅负责应更新的TextView。

  2. Other approach ( less efficient ) is to use ListView and not to re-use the cells to create new one for each item. 另一种方法(效率较低)是使用ListView,而不是重复使用单元格为每个项目创建新的单元格。 This will cause performance issues 这将导致性能问题

If you can send some code i would like to help you out. 如果您可以发送一些代码,我想为您提供帮助。

*** EDITED **** ***编辑****

Here is some code part that can make things more clear 这是一些代码部分,可以使事情更清楚

@Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        holder.songDurationView.setTag(position);
        //TODO: implement some more logic here and start the MusicSongRunnable
    }



class MusicSongRunnable implements Runnable {

    int positionOfSong;
    TextView textView;

    public MusicSongRunnable(int positionOfSong, TextView textView) {
        this.positionOfSong = positionOfSong;
        this.textView = textView;
    }

    @Override
    public void run() {
        if (player.isPlaying() && positionOfSong == textView.getTag()) {
        //TODO: update the song;
    }
}

It is not clear where you call updateTimer , but I assume that you call it from within callback.onPlayClickListener(..) and callback.onPauseClickListener(..) . 目前尚不清楚在哪里调用updateTimer ,但我假设您是从callback.onPlayClickListener(..)callback.onPauseClickListener(..)内部callback.onPlayClickListener(..)

First , do not pass it the position you've got as a parameter in click listeners, but as documentation states you should use ViewHolder.getAdapterPosition() method. 首先 ,不通过它的position你有在点击监听器的参数,但文档指出 ,你应该使用ViewHolder.getAdapterPosition()方法。 Note, that when you use the value of the position you should also check that it is different from RecuclerView.NO_POSITION each time and only then use it. 请注意,当您使用位置值时,还应该每次都检查它是否与RecuclerView.NO_POSITION不同,然后才使用它。 Make your position parameter not final in all methods. 使您的位置参数在所有方法中都不是最终的。 It will prevent you from making mistakes. 这样可以防止您犯错误。 Each time you want to use position in click listeners use myChatViewHolder.getAdapterPosition() 每次您想在点击侦听器中使用位置时,请使用myChatViewHolder.getAdapterPosition()

Second , since you cache position value in the Runnable anyway, this is probably not enough. 其次 ,由于无论如何都要在Runnable中缓存position值,所以这可能还不够。 So you should pass myChatViewHolder.timer as a parameter to updateTimer and get rid of first two lines. 因此,您应该将myChatViewHolder.timer作为参数传递给updateTimer并摆脱前两行。 In the end you call updateTimer like this: 最后,您像这样调用updateTimer

updateTimer(myChatViewHolder.timer);

and your 'updateTimer' now is: 现在您的“ updateTimer”为:

public void updateTimer(final TextView timer) {

    r = new Runnable() {
        public void run() {
            int currentDuration;
            if (player.isPlaying()) {
                currentDuration = player.getCurrentPosition();
                timer.setText("" + milliSecondsToTimer((long) currentDuration));
                timer.postDelayed(this, 1000);
            } else {
                timer.removeCallbacks(this);
            }
        }
    };

    timer.post(r);
}

More from the documentation on this matter: 文档中有关此问题的更多信息:

RecyclerView will not call onBindViewHolder() method again if the position of the item changes in the data set unless the item itself is invalidated or the new position cannot be determined. 如果项目的位置在数据集中更改,则RecyclerView将不会再次调用onBindViewHolder()方法,除非该项目本身无效或无法确定新位置。 For this reason, you should only use the position parameter while acquiring the related data item inside this method and should not keep a copy of it. 因此,您仅应在获取此方法内的相关数据项时使用position参数,而不应保留其副本。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM