【问题标题】:Recyclerview reverse layout starting scroll position issueRecyclerview反向布局起始滚动位置问题
【发布时间】:2019-07-06 02:31:08
【问题描述】:

我正在创建一个聊天应用程序并显示我正在使用回收器视图的消息。最新消息显示在底部。用户向上滚动以查看更多消息。

加载聊天屏幕时,视图不会从最底部开始,并且最新消息不可见。用户必须向下滚动几行才能看到最新消息。这是糟糕的用户界面,最新消息应该显示在屏幕的末尾/底部。

我正在使用setReverseLayout(true)setStackFromEnd(false),并且我在网上搜索过类似的问题,但没有成功。目前,我在延迟设置回收站视图后将滚动位置设置为 0,但这并不总是有效,而且很跳跃。

如果我正常设置recyclerview(不使用setReverseLayoutsetStackFromEnd),最新消息每次都完美地加载在顶部。

这是启动回收器视图的代码:

RecyclerView recyclerView = findViewById(R.id.recyclerViewMessageRoom);
    adapter = new RA_MessageRoom(this, userFirebaseUid, messagesGroupedByDate, messageUsers);
    recyclerView.setAdapter(adapter);

    LinearLayoutManager layoutManager = new LinearLayoutManager(this);
    layoutManager.setReverseLayout(true);
    layoutManager.setStackFromEnd(false);
    recyclerView.setHasFixedSize(true);
    recyclerView.setLayoutManager(layoutManager);

    // -- Workaround with delay - still doesn't completely work
    new Handler().postDelayed(() -> {
        recyclerView.scrollToPosition(0);
    }, 200);

任何遇到过此问题并知道如何解决的人请分享!谢谢。

编辑(2019 年 7 月 6 日):

这是回收器适配器的代码。提醒一下,如果我删除反向布局设置,它工作得很好。

RA_MessageRoom:

public class RA_MessageRoom extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final String TAG = "RA_MessageRoom";
private static final int TYPE_USER = 1;
private static final int TYPE_PARTICIPANT = 2;

private Context context;
private String userFirebaseUid;
private List<MessagesDateGrouper> messagesGroupedByDate;
private HashSet<MessagesUserModel> messageUsers;

public RA_MessageRoom(Context context, String userFirebaseUid, List<MessagesDateGrouper> messagesGroupedByDate, HashSet<MessagesUserModel> messageUsers) {
    this.context = context;
    this.userFirebaseUid = userFirebaseUid;
    this.messagesGroupedByDate = messagesGroupedByDate;
    this.messageUsers = messageUsers;
}

@Override
public int getItemViewType(int position) {

    if (messagesGroupedByDate.get(position).getViewType() == MessagesDateGrouper.TYPE_CHAT) {
        MessageChatItem message = (MessageChatItem) messagesGroupedByDate.get(position);
        if (message.getMessages().getSenderFirebaseUid().equals(userFirebaseUid)) {
            return TYPE_USER;
        } else {
            return TYPE_PARTICIPANT;
        }
    } else {
        return messagesGroupedByDate.get(position).getViewType();
    }
}


@Override
public int getItemCount() {
    return messagesGroupedByDate != null ? messagesGroupedByDate.size() : 0;
}

@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    final RecyclerView.ViewHolder holder;
    View view;

    switch (viewType) {
        case MessagesDateGrouper.TYPE_DATE:
            view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_message_room_separator, parent, false);
            holder = new MessageRoomDateVH(view);
            break;

        case TYPE_USER:
            view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_message_room_user, parent, false);
            holder = new MessageRoomUserVH(view);
            break;

        case TYPE_PARTICIPANT:
            view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_message_room_participant, parent, false);;
            holder = new MessageRoomParticipantVH(view);
            break;

        default:
            view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_message_room_user, parent, false);;
            holder = new MessageRoomUserVH(view);
            break;
    }

    return holder;
}

@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
    if (holder instanceof MessageRoomDateVH) {
        MessageDateItem date = (MessageDateItem) messagesGroupedByDate.get(position);
        ((MessageRoomDateVH)holder).date.setText(date.getDate());

    } else if (holder instanceof MessageRoomParticipantVH) {
        MessageRoomParticipantVH view = (MessageRoomParticipantVH) holder;
        MessageChatItem messageItem = (MessageChatItem) messagesGroupedByDate.get(position);
        MessagesModel message = messageItem.getMessages();
        
        view.name.setText(context.getString(R.string.unknown));
        view.profileImage.setImageResource(R.drawable.default_profile_image_grey);

        for (MessagesUserModel user: messageUsers) {
            if (user.getFirebaseId().equals(message.getSenderFirebaseUid())) {
                int fallbackImage;
                if (user.getMerchant() == null || !user.getMerchant()) {
                    fallbackImage = R.drawable.default_profile_image_grey;
                } else {
                    fallbackImage = R.drawable.store_profile;
                }

                GlideApp.with(context)
                        .load(user.getPhotoThumbUrl())
                        .placeholder(R.drawable.placeholder)
                        .fallback(fallbackImage)
                        .into(view.profileImage);

                view.name.setText(user.getName());
                break;
            }
        }


        if (message.getImageUrl() != null && !message.getImageUrl().equals("") ) {
            view.image.setVisibility(View.VISIBLE);
            view.imageSpinner.setVisibility(View.VISIBLE);
            view.chat.setVisibility(View.GONE);



            view.image.setClipToOutline(true);

            GlideApp.with(context)
                    .load(message.getImageUrl())
                    .placeholder(R.drawable.placeholder_message)
                    .fallback(R.drawable.placeholder_message)
                    .listener(new RequestListener<Drawable>() {
                        @Override
                        public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
                            return false;
                        }

                        @Override
                        public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
                            view.imageSpinner.setVisibility(View.GONE);
                            return false;
                        }
                    })
                    .into(view.image);


        } else {
            view.chat.setVisibility(View.VISIBLE);
            view.image.setVisibility(View.GONE);
            view.imageSpinner.setVisibility(View.GONE);

            view.chat.setText(message.getMessageText());
        }

        String time = DateFormatService.messageRoomParseDateToTimeString(message.getDate());
        view.date.setText(time);

    } else if (holder instanceof MessageRoomUserVH){
        MessageRoomUserVH view = (MessageRoomUserVH) holder;
        MessageChatItem messageItem = (MessageChatItem) messagesGroupedByDate.get(position);
        MessagesModel message = messageItem.getMessages();


        if (message.getImageUrl() != null && !message.getImageUrl().equals("") ) {
            view.image.setVisibility(View.VISIBLE);
            view.imageSpinner.setVisibility(View.VISIBLE);
            view.chat.setVisibility(View.GONE);

            view.image.setClipToOutline(true);

            GlideApp.with(context)
                    .load(message.getImageUrl())
                    .placeholder(R.drawable.placeholder_message)
                    .fallback(R.drawable.placeholder_message)
                    .listener(new RequestListener<Drawable>() {
                        @Override
                        public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
                            return false;
                        }

                        @Override
                        public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
                            view.imageSpinner.setVisibility(View.GONE);
                            return false;
                        }
                    })
                    .into(view.image);


        } else {
            view.chat.setVisibility(View.VISIBLE);
            view.image.setVisibility(View.GONE);
            view.imageSpinner.setVisibility(View.GONE);

            view.chat.setText(message.getMessageText());
        }

        String time = DateFormatService.messageRoomParseDateToTimeString(message.getDate());
        view.date.setText(time);
    }


}

public class MessageRoomDateVH extends RecyclerView.ViewHolder {
    TextView date;

    public MessageRoomDateVH(@NonNull View itemView) {
        super(itemView);
        date = itemView.findViewById(R.id.textMessageRoomDateSection);
    }
}


public class MessageRoomParticipantVH extends RecyclerView.ViewHolder {
    ImageView profileImage;
    TextView name;
    TextView chat;
    ImageView image;
    TextView date;
    ProgressBar imageSpinner;

    public MessageRoomParticipantVH(@NonNull View itemView) {
        super(itemView);

        profileImage = itemView.findViewById(R.id.imageMessageRoomParticipantProfile);
        name = itemView.findViewById(R.id.textMessageRoomParticipantName);
        chat = itemView.findViewById(R.id.textMessageRoomParticipantChat);
        image = itemView.findViewById(R.id.imageMessageRoomParticipantImage);
        date = itemView.findViewById(R.id.textMessageRoomParticipantDate);
        imageSpinner = itemView.findViewById(R.id.progressBarMessageRoomParticipantImage);


    }
}

public class MessageRoomUserVH extends RecyclerView.ViewHolder {
    TextView chat;
    ImageView image;
    TextView date;
    ProgressBar imageSpinner;

    public MessageRoomUserVH(@NonNull View itemView) {
        super(itemView);

        chat = itemView.findViewById(R.id.textMessageRoomUserChat);
        image = itemView.findViewById(R.id.imageMessageRoomUserImage);
        date = itemView.findViewById(R.id.textMessageRoomUserDate);
        imageSpinner = itemView.findViewById(R.id.progressBarMessageRoomUserImage);
    }
}

}

编辑:

虽然首选是将 stackFromEnd 设置为 false,但我最终将 stackFromEnd 从 false 更改为 true,消除了延迟,并按照接受的答案的建议继续设置滚动位置以解决此问题。

更新的工作代码:

RecyclerView recyclerView = findViewById(R.id.recyclerViewMessageRoom);
    adapter = new RA_MessageRoom(this, userFirebaseUid, messagesGroupedByDate, messageUsers);
    recyclerView.setAdapter(adapter);

    LinearLayoutManager layoutManager = new LinearLayoutManager(this);
    layoutManager.setReverseLayout(true);
    layoutManager.setStackFromEnd(true);
    recyclerView.setHasFixedSize(true);
    recyclerView.setLayoutManager(layoutManager);
    recyclerView.scrollToPosition(0);

感谢您的帮助!

您还需要处理首页加载时的消息加载以及收到的新消息位于 recyclerview 底部

if (pageLoad) { 
 
 list.add(Model)

} else {
 
 // Add to the top of the list (since list is reverse message will come at the bottom)
  list.add(0, Model)

}

【问题讨论】:

  • 你能显示你的适配器代码吗?问题可能是其他问题,因为上面的代码看起来不错
  • 使用 LinearLayoutManager linearLayoutManager = new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true); recyclerView.scrollToPosition(adapter.getItemCount() - 1);
  • @SyedAhmedJamil 我已经添加了适配器代码。如果您有任何想法,将不胜感激。
  • @sajadzohrei 我尝试按照您的方式设置 LinearLayoutManager,但问题仍然存在。

标签: android android-recyclerview


【解决方案1】:

这很容易,您只需添加一行:-

LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
linearLayoutManager.setStackFromEnd(true);
recyclerView.setLayoutManager(linearLayoutManager);

【讨论】:

  • 与我的代码的唯一区别是您已将 stackFromEnd 设置为 true,而我将其设置为 false。我需要将其设置为 false 才能加载到底部。但是,即使进行了这种更改,问题仍然存在。
  • 更正我的评论,如果我使用 linearLayoutManager.setStackFromEnd(true) 并设置 recyclerView.scrollToPosition(0),它可以工作。我最初在没有设置 scrollToPosition 的情况下测试了您的答案。尽管我们更喜欢从底部堆叠它,但我会将 setStackFromEnd 更改为 true 以解决此问题。谢谢。
【解决方案2】:

注意:接受的答案会误导您。

LinearLayoutManager的上市热情有4种可能。

    1. 
     startStackFromEnd=true
     reverseLayout=true
    2.
     startStackFromEnd=false
     reverseLayout=false
    3. 
     startStackFromEnd=true
     reverseLayout=false
    4. 
     startStackFromEnd=false //best for chatting
     reverseLayout=true      //applications

每个组合都有不同的作用,我不知道你的具体要求是什么,所以围绕这些值进行调整,我相信你会得到你想要的。

【讨论】:

    【解决方案3】:

    看到这个答案:

    RecyclerView - Reverse Order

    并为您的RA_MessageRoom 创建一个设置器以更新您的messagesGroupedByDate。类似的东西:

    Collections.reverse(messagesGroupedByDate); // Reverse your dataset like in answer above
    adapter.setMessagesGroupedByDate(messagesGroupedByDate); // Update your dataset in adapter
    adapter.notifyDataSetChanged(); // Notify your adapter
    

    每次有新消息到达时,您的列表都会更新。您需要将此 sn-p 放入数据提取中,然后您可以删除您的 postDelayed 处理程序。

    【讨论】:

    • 已经用树形图对数据进行排序,并在适配器中添加了setter。设置消息,然后调用 notifyDataSetChanged,但问题仍然存在。还测试了在消息加载和排序后用 10 秒计时器启动回收器视图,但它仍然没有从底部完全加载...
    【解决方案4】:

    您可以在 xml 中设置布局管理器和反向布局。当我们设置反向布局时,我们发现了这个问题。我们可以通过添加 setStackFromEnd =true

    来解决它
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        android:orientation="vertical"
        app:reverseLayout="true"
        app:stackFromEnd="true"
    

    【讨论】:

      【解决方案5】:

      看起来像 bug in recycler view 实现,或者更确切地说是在线性布局管理器中。

      要解决此问题,您需要确保 stackFromEnd 在您的回收站可以滚动时为真。如果您的回收站没有足够的项目来滚动并且您仍然希望它们位于屏幕底部,那么如果回收站无法滚动,请将 stackFromEnd 设置为 false。

      要判断您的视图是否可以滚动,您可以使用 Kotlin 扩展:

      /**
       * Tells if this view can scroll vertically.
       * This view may still contain children who can scroll.
       */
      fun View.canScrollVertically() = this.let {
          it.canScrollVertically(-1) || it.canScrollVertically(1)
      }
      

      Fulguris 中,我使用以下函数来修复该错误,诀窍是在您的代码中找到正确的位置来调用它:

      /**
       * Workaround reversed layout bug: https://github.com/Slion/Fulguris/issues/212    
       */
      fun fixScrollBug(aList : RecyclerView): Boolean {
          val lm = (aList.layoutManager as LinearLayoutManager)
          // Can't change stackFromEnd when computing layout or scrolling otherwise it throws an exception
          if (!aList.isComputingLayout) {
              if (aList.context.configPrefs.toolbarsBottom) {
                  // Workaround reversed layout bug: https://github.com/Slion/Fulguris/issues/212
                  if (lm.stackFromEnd != aList.canScrollVertically()) {
                      lm.stackFromEnd = !lm.stackFromEnd
                      return true
                  }
              } else {
                  // Make sure this is set properly when not using bottom toolbars
                  // No need to check if the value is already set properly as this is already done internally
                  lm.stackFromEnd = false
              }
          }
      
          return false
      }
      

      我猜你可以将toolbarsBottom 上的应用程序特定检查替换为针对lm.reverseLayout 的检查。

      【讨论】:

        【解决方案6】:

        遇到了同样的问题 - 提交列表总是会导致显示的项目位于列表中间的某个位置,而不是第一个项目显示在底部。这发生在 app:reverseLayout="true" 但经常用于 non-reversed 时。意识到我的 recyclerview 在约束布局内的高度为 0dp,约束从顶部到父级,从底部到父级。将高度更改为 match_parent 并且它可以正常工作 - 现在底部的第一项是提交列表时的起点。

        【讨论】:

        • 这并不能真正回答问题。如果您有其他问题,可以点击 提问。要在此问题有新答案时收到通知,您可以follow this question。一旦你有足够的reputation,你也可以add a bounty 来引起对这个问题的更多关注。 - From Review
        猜你喜欢
        • 2020-10-04
        • 2020-01-16
        • 1970-01-01
        • 2021-05-17
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-05-22
        • 1970-01-01
        相关资源
        最近更新 更多