【问题标题】:How can I properly center the first and last items in a horizontal RecyclerView如何在水平 RecyclerView 中正确居中第一个和最后一个项目
【发布时间】:2021-02-27 09:26:58
【问题描述】:

StackOverflow 包含很多这样的问题,但到目前为止,绝对没有一个解决方案能 100% 奏效。

我尝试了这些解决方案:

RecyclerView ItemDecoration - How to draw a different width divider for every viewHolder?

first item center aligns in SnapHelper in RecyclerView

Horizontally center first item of RecyclerView

LinearSnapHelper doesn't snap on edge items of RecyclerView

Android Centering Item in RecyclerView

How to make recycler view start adding items from center?

How to have RecyclerView snapped to center and yet be able to scroll to all items, while the center is "selected"?

How to snap to particular position of LinearSnapHelper in horizontal RecyclerView?


并且每次第一项无法正确居中。

我正在使用的示例代码

    recyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
        @Override
        public void getItemOffsets(
            @NonNull Rect outRect,
            @NonNull View view,
            @NonNull RecyclerView parent,
            @NonNull RecyclerView.State state
        ) {

            super.getItemOffsets(outRect, view, parent, state);

            final int count = state.getItemCount();
            final int position = parent.getChildAdapterPosition(view);

            if (position == 0 || position == count - 1) {

                int offset = (int) (parent.getWidth() * 0.5f - view.getWidth() * 0.5f);

                if (position == 0) {
                    setupOutRect(outRect, offset, true);
                } else if (position == count - 1) {
                    setupOutRect(outRect, offset, false);
                }

            }

        }

        private void setupOutRect(Rect rect, int offset, boolean start) {
            if (start) {
                rect.left = offset;
            } else {
                rect.right = offset;
            }
        }

    });

调查后发现是因为getItemOffsets的时候view.getWidth是0,还没有测出来。

我试图强制测量它,但每次它给出的尺寸都不正确,与它实际占据的尺寸完全不同,它更小。

我也尝试使用 addOnGlobalLayoutListener 技巧,但是当它被调用并具有正确的宽度时,outRect 已经被消耗,所以它丢失了。

我不想设置任何固定大小,因为RecyclerView 中的项目可以有不同的大小,因此不能提前设置其填充。

我也不想添加“幽灵”项目来填充空间,这些项目也不能很好地用于滚动体验。

我怎样才能让它正常工作?

理想情况下,ItemDecorator 方法看起来是最好的,但对于第一项,它马上就落伍了。

【问题讨论】:

    标签: android android-recyclerview centering item-decoration


    【解决方案1】:

    我一直在使用来自Pawel's answerCenterLinearLayoutManager,到目前为止它工作得几乎完美。我说几乎,不是因为它不能立即工作,而是因为我最终在使用上述布局管理器的同一 RecyclerView 中使用了 LinearSnapHelper

    因为 snap 助手依赖于知道RecyclerView 填充来计算其正确的中心,所以只设置第一项的填充会抛出这个过程,导致第一项(以及后续的项目,直到最后一个实际显示)从中心偏移。

    我的解决方案是确保在显示第一项时从一开始就设置两个填充。

    所以这个:

    if (!reverseLayout) {
        if (lp == 0) recyclerView.updatePaddingRelative(start = hPadding)
        if (lp == itemCount - 1) recyclerView.updatePaddingRelative(end = hPadding)
      } else {
        if (lp == 0) recyclerView.updatePaddingRelative(end = hPadding)
        if (lp == itemCount - 1) recyclerView.updatePaddingRelative(start = hPadding)
      }
    

    变成这样

    if (!reverseLayout) {
        if (lp == 0) recyclerView.updatePaddingRelative(start = hPadding, end = hPadding) // here we set the same padding for both sides
        if (lp == itemCount - 1) recyclerView.updatePaddingRelative(end = hPadding)
       } else {
        if (lp == 0) recyclerView.updatePaddingRelative(end = hPadding, start = hPadding) // here we set the same padding for both sides
        if (lp == itemCount - 1) recyclerView.updatePaddingRelative(start = hPadding)
       }
    

    我假设垂直块也必须应用相同的逻辑。

    我相信现在可以进一步优化,所以上面的最终块看起来像这样:

    if (lp == 0) recyclerView.updatePaddingRelative(start = hPadding, end = hPadding) // here we set the same padding for both sides
    if (lp == itemCount - 1) {
       if (!reverseLayout) recyclerView.updatePaddingRelative(end = hPadding)
       if (reverseLayout) recyclerView.updatePaddingRelative(start = hPadding)
    }
    

    注意:我最终发现,如果我使用它们之间存在显着宽度差异的项目,我在此处提到的相同问题也会在最后一个项目加载时发生,这是有道理的,因为此时我们在一侧设置了不同的填充比另一个,再次抛出原生中心计算。

    这个解决方案是在第一个项目加载时为两边设置相同的填充,最后一个也是这样

    recyclerView.updatePaddingRelative(start = hPadding, end = hPadding)
    

    字面意思就是这样,没有像上一个示例中那样的 if。

    当然,当显示的项目太少以至于第一个和最后一个项目可见时,这仍然不能解决问题,但是当我设法找到针对该特定情况的解决方案时,我会更新它在这里。

    【讨论】:

      【解决方案2】:

      您也可以更改RecyclerView 本身的填充来获得这种效果(只要clipToPadding 被禁用)。我们可以在LayoutManager 中拦截第一个布局阶段,这样即使在第一次布局项目时它也可以使用更新的填充:

      添加此布局管理器:

      open class CenterLinearLayoutManager : LinearLayoutManager {
          constructor(context: Context) : super(context)
          constructor(context: Context, orientation: Int, reverseLayout: Boolean) : super(context, orientation, reverseLayout)
          constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
      
          private lateinit var recyclerView: RecyclerView
      
          override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
              // always measure first item, its size determines starting offset
              // this must be done before super.onLayoutChildren
              if (childCount == 0 && state.itemCount > 0) {
                  val firstChild = recycler.getViewForPosition(0)
                  measureChildWithMargins(firstChild, 0, 0)
                  recycler.recycleView(firstChild)
              }
              super.onLayoutChildren(recycler, state)
          }
      
          override fun measureChildWithMargins(child: View, widthUsed: Int, heightUsed: Int) {
              val lp = (child.layoutParams as RecyclerView.LayoutParams).absoluteAdapterPosition
              super.measureChildWithMargins(child, widthUsed, heightUsed)
              if (lp != 0 && lp != itemCount - 1) return
              // after determining first and/or last items size use it to alter host padding
              when (orientation) {
                  HORIZONTAL -> {
                      val hPadding = ((width - child.measuredWidth) / 2).coerceAtLeast(0)
                      if (!reverseLayout) {
                          if (lp == 0) recyclerView.updatePaddingRelative(start = hPadding)
                          if (lp == itemCount - 1) recyclerView.updatePaddingRelative(end = hPadding)
                      } else {
                          if (lp == 0) recyclerView.updatePaddingRelative(end = hPadding)
                          if (lp == itemCount - 1) recyclerView.updatePaddingRelative(start = hPadding)
                      }
                  }
                  VERTICAL -> {
                      val vPadding = ((height - child.measuredHeight) / 2).coerceAtLeast(0)
                      if (!reverseLayout) {
                          if (lp == 0) recyclerView.updatePaddingRelative(top = vPadding)
                          if (lp == itemCount - 1) recyclerView.updatePaddingRelative(bottom = vPadding)
                      } else {
                          if (lp == 0) recyclerView.updatePaddingRelative(bottom = vPadding)
                          if (lp == itemCount - 1) recyclerView.updatePaddingRelative(top = vPadding)
                      }
                  }
              }
          }
      
          // capture host recyclerview
          override fun onAttachedToWindow(view: RecyclerView) {
              recyclerView = view
              super.onAttachedToWindow(view)
          }
      }
      

      然后将它用于您的 RecyclerView:

      recyclerView.layoutManager = CenterLinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
      recyclerView.clipToPadding = false // disabling clip to padding is critical
      

      【讨论】:

      • 我不知道为什么当我几乎在您发布的同一天接受它时,为什么不接受它作为答案,但希望这次它会保持正确标记。另外 - 工作得很好,非常感谢你的解释,我真的很重视他们。
      • 你能帮我澄清一下吗?我已经使用了我认为与val lp = 等效的值,但只是为了确保您能解释一下您在该行上传递的意思吗?因为布局参数没有absoluteAdapterPosition 选项
      • 存在于RecyclerView.LayoutParams。我已经发布了将 getter 转换为属性的 kotlin 代码,如果您使用的是 java,则需要显式输入 getAbsoluteAdapterPosition()。如果它不存在,请确保您的 recyclerview 导入是最新的,因为它是最近添加的,以支持弃用的 getViewAdapterPosition
      • 这很奇怪,对我来说它显示为一个不存在的选项(我什至在你的代码中使用它,在 kotlin 中)。也许我必须导入我不知道的特定版本的 RecyclerView?
      • 对此的一个改进是,如果我们想使用任何 snap pager helper,必须在为第一项设置时设置开始和结束填充,否则第一项将始终偏移一点,直到recyclerview 在两边都有一个填充集,因为只有一个会抛出它的中心位置计算。
      【解决方案3】:

      据我所知,您希望您的第一个和最后一个项目位于 recyclerview 的中心。如果是这样,我会推荐一个更简单的解决方法。

      public class OverlaysAdapter extends RecyclerView.Adapter<OverlaysAdapter2.CategoryViewHolder> {
      
      
      private int fullWidth;//gets the recyclerview full width in constructor. In my case it is full display width.
      
      @Override
      public CategoryViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
          FrameLayout fr = (FrameLayout) inflater.inflate(R.layout.item_sticker, parent, false);
          if (viewType==TYPE_LEFT_ITEM) {
              int marginLeft = (fullWidth-leftItemWidth)/2;
              ((RecyclerView.LayoutParams)fr.getLayoutParams()).setMargins(marginLeft, 0,0,0);
          } else if (viewType==TYPE_RIGHT_ITEM) {
              int marginRight = (fullWidth-rightItemWidth)/2;
              ((RecyclerView.LayoutParams)fr.getLayoutParams()).setMargins(0, 0,marginRight,0);
          } else if (viewType==TYPE_MIDDLE_ITEM) {
              ((RecyclerView.LayoutParams)fr.getLayoutParams()).setMargins(0, 0,0,0);
          }
          return new CategoryViewHolder(fr);
      }
      
      
      private final int TYPE_LEFT_ITEM = 1;
      private final int TYPE_MIDDLE_ITEM = 2;
      private final int TYPE_RIGHT_ITEM = 3;
      
      @Override
      public int getItemViewType(int position) {
          if (position==0)
              return TYPE_RIGHT_ITEM;
          else if (position==items.size()-1)
              return TYPE_LEFT_ITEM;
          else
              return TYPE_MIDDLE_ITEM;
      }
      
      }
      

      这个想法很简单。为第一项、中间项和最后一项定义三种视图类型。注意项目视图类型并计算所需的边距并设置边距。

      请注意,在我的例子中,左边距用于最后一项,右边距用于第一项,因为布局始终为RTL。您可能想使用相反的顺序。

      【讨论】:

      • 你真的测试过你自己的建议吗? onCreateViewHolder 项目的宽度也返回 0,因为它甚至还没有被布置。这意味着(fullWidth-fr.getWidth())/2 将始终等于fullWidth/2,因为fr.getWidth() 始终返回0。
      • 我测试过,除了项目宽度外它都有效。无论如何,如果您的每件商品都有固定尺寸,您可以更换它。否则这个技巧对你不起作用。
      • 我在我的问题中确实说过“我不想设置任何固定大小,因为 RecyclerView 中的项目可以有不同的大小 (...)”
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-08-02
      • 2019-01-01
      • 2019-03-12
      • 1970-01-01
      • 1970-01-01
      • 2016-03-06
      相关资源
      最近更新 更多