美文网首页
RecyclerView的Item没有充满整个宽度

RecyclerView的Item没有充满整个宽度

作者: 就叫汉堡吧 | 来源:发表于2022-03-02 14:03 被阅读0次
    • 概述

      在一开始使用RecyclerView的过程中,可能会遇到这么一种情况,就是我们的item View已经设置成match_parent了,RecyclerView也设置成match_parent,但是在显示的时候却只显示内容wrap_content的大小,布局文件没有问题,那么么问题出在哪呢?

      分析一下,可以猜测,只有在item添加到RecyclerView时的中间部分才有可能修改LayoutParams,也就是问题最有可能出现在onCreateViewHolder中的inflate的时候。

    • View.inflate

      通常为了简单,我们经常调用View.inflate来加载布局,而我们出现上面问题的时候也是使用的这个,这个方法定义如下:

      public static View inflate(Context context, @LayoutRes int resource, ViewGroup root) {
          LayoutInflater factory = LayoutInflater.from(context);
          return factory.inflate(resource, root);
      }
      

      因为我们是要加到RecyclerView中去,在RecyclerView的布局工作中会调用addView添加,所以我们这里的root必须传null,否则就会抛出“不允许有多个parent”的异常。

      可以看到,这里其实就是对于LayoutInflater.from(context).inflate(@LayoutRes int resource, @Nullable ViewGroup root)方法的封装:

      public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
          return inflate(resource, root, root != null);
      }
      

      这个方法又是调用了三个参数的inflate重载方法:

      public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
          final Resources res = getContext().getResources();
          if (DEBUG) {
              Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                    + Integer.toHexString(resource) + ")");
          }
      
          View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
          if (view != null) {
              return view;
          }
          XmlResourceParser parser = res.getLayout(resource);
          try {
              return inflate(parser, root, attachToRoot);
          } finally {
              parser.close();
          }
      }
      

      因为我们这里传入的root是null,所以attachToRoot为false,这里调用了tryInflatePrecompiled方法创建View:

      private @Nullable
      View tryInflatePrecompiled(@LayoutRes int resource, Resources res, @Nullable ViewGroup root,
          boolean attachToRoot) {
          if (!mUseCompiledView) {
              return null;
          }
      
          Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate (precompiled)");
      
          // Try to inflate using a precompiled layout.
          String pkg = res.getResourcePackageName(resource);
          String layout = res.getResourceEntryName(resource);
      
          try {
              Class clazz = Class.forName("" + pkg + ".CompiledView", false, mPrecompiledClassLoader);
              Method inflater = clazz.getMethod(layout, Context.class, int.class);
              View view = (View) inflater.invoke(null, mContext, resource);
      
              if (view != null && root != null) {
                  // We were able to use the precompiled inflater, but now we need to do some work to
                  // attach the view to the root correctly.
                  XmlResourceParser parser = res.getLayout(resource);
                  try {
                      AttributeSet attrs = Xml.asAttributeSet(parser);
                      advanceToRootNode(parser);
                      ViewGroup.LayoutParams params = root.generateLayoutParams(attrs);
      
                      if (attachToRoot) {
                          root.addView(view, params);
                      } else {
                          view.setLayoutParams(params);
                      }
                  } finally {
                      parser.close();
                  }
              }
      
              return view;
          } catch (Throwable e) {
              if (DEBUG) {
                  Log.e(TAG, "Failed to use precompiled view", e);
              }
          } finally {
              Trace.traceEnd(Trace.TRACE_TAG_VIEW);
          }
          return null;
      }
      

      可以看到,这里利用反射,找到View类中View(Context.class, int.class)这个构造函数来生成View实例,而View的构造函数中并没有任何关于LayoutParams的操作,往下看,if语句判断中null不为空这个条件不符合,所以不会执行if语句内的代码,if语句内恰好是把我们xml布局中定义的LayoutParams属性设置到View的操作,所以,因为root为null的关系,我们在布局中设置的layout_xxx相关的属性并没有被使用,这里只是返回一个基本构造函数返回的View实例。

      从这我们也能看出来,LayoutParams是有关子View在父容器中存在的效果的,所以这里的父容器为空时也不需要处理LayoutParams。但有人会说,子View本身也有需要显示的内容啊,如果不设置LayoutParams的话那内容也显示不出来了啊。是的,LayoutParams是必须要设置的,只是..不是现在,接着往下看。

    • RecyclerView的处理

      创建了View实例之后,返回onCreateViewHolder方法,这个方法是在RecyclerView的layout流程中的tryGetViewHolderForPositionByDeadline方法中调用的,它里面有这样一段代码:

      final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
      final LayoutParams rvLayoutParams;
      if (lp == null) {
          rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
          holder.itemView.setLayoutParams(rvLayoutParams);
      } else if (!checkLayoutParams(lp)) {
          rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
          holder.itemView.setLayoutParams(rvLayoutParams);
      } else {
          rvLayoutParams = (LayoutParams) lp;
      }
      

      可以看到,如果View没有设置LayoutParams的话会调用generateDefaultLayoutParams方法设置,这个方法内部是调用了对应LayoutManager的同名方法,以LinearLayoutManager为例:

      @Override
      public RecyclerView.LayoutParams generateDefaultLayoutParams() {
          return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                  ViewGroup.LayoutParams.WRAP_CONTENT);
      }
      

      可见,这里默认会使用WRAP_CONTENT作为宽高的LayoutParams属性,这也就能解释了为什么会出现Item没有充满RecyclerView宽(高)的情况。

    • 其他

      假使我们添加了DividerItemDecoration(ColorDrawable(Color.GRAY)),并且错误地使用了View.inflate方法加载,那么会出现这么一种情况:

      image-20211206144221245

      灰色的是我们添加的分割线,卡其色是Item的背景色,也就是内容区域,可以看到,分割线被内容挡住了一部分,这是为什么呢?而分割线为什么又是这么高呢?

      DividerItemDecoration的getItemOffsets方法如下:

      @Override
      public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
              RecyclerView.State state) {
          if (mDivider == null) {
              outRect.set(0, 0, 0, 0);
              return;
          }
          if (mOrientation == VERTICAL) {
              outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
          } else {
              outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
          }
      }
      

      mDivider是设置的Drawable,也就是ColorDrawable,它没有重写Drawable的getIntrinsicHeight方法:

      public int getIntrinsicHeight() {
          return -1;
      }
      

      所以这就是高度为什么是1像素的原因。

      我们知道在RecyclerView的measure流程中会调用getItemOffsets方法把分割线的高度作为child的一部分提前预留出来,这里是-1,所以取最大范围的值,也就是child本身设置的大小,这里在onDraw中又通过bottom-mDivider.getIntrinsicHeight()赋值给分割线的top:

      for (int i = 0; i < childCount; i++) {
          final View child = parent.getChildAt(i);
          parent.getDecoratedBoundsWithMargins(child, mBounds);
          final int bottom = mBounds.bottom + Math.round(child.getTranslationY());
          final int top = bottom - mDivider.getIntrinsicHeight();
          mDivider.setBounds(left, top, right, bottom);
          mDivider.draw(canvas);
      }
      

      所以会出现内容覆盖分割线一部分的效果。

    • 总结

      根据以上分析,我们得出以下结论:

      对于RecyclerView,在加载Item布局时,我们要使用LayoutInflater.from(context).inflate(R.layout.xxxxx, parent,false)来加载,这个方法可以保证root不为null同时attchToRoot为false,也就能保证既可以应用了我们布局中设置的layout属性,又不会产生“不允许存在多个parent”的异常。

    相关文章

      网友评论

          本文标题:RecyclerView的Item没有充满整个宽度

          本文链接:https://www.haomeiwen.com/subject/uiulrrtx.html