1.前言
RecyclerView的item很多情况下都是需要有分割线的或者说是彼此之间需要有间隔。
如下图示例,每个item大小一致,假设彼此之间的分割线宽度为20dp,分割线是透明的。那么此时分割线的作用更多是作为item之间的间隔。
2.有问题的实现
通常我们的做法就是在RecyclerView中添加DividerItemDecoration
,这是实现了RecyclerView库中帮我们默认实现了ItemDecoration
这个抽象类的一个类。使用方法就是通过setDrawable(@NonNull Drawable drawable)
方法设置分割线的样式。
//RecyclerView所在的布局
# activity_main.xml
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/mRv"
android:layout_marginTop="20dp"
android:layout_width="0dp"
android:padding="1dp"
android:layout_height="wrap_content"
android:background="@drawable/rv_bg"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
//间隔的xml实现,宽度20dp,颜色透明
#item_divider_shape.xml
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<size android:width="20dp" />
<solid android:color="@android:color/transparent" />
</shape>
#MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val adapter = ItemAdapter()
mRv.adapter = adapter
mRv.layoutManager = LinearLayoutManager(baseContext, RecyclerView.HORIZONTAL, false)
mRv.addItemDecoration(
DividerItemDecoration(
baseContext,
DividerItemDecoration.HORIZONTAL
).apply {
setDrawable(resources.getDrawable(R.drawable.item_divider_shape))
})
val list = mutableListOf<String>()
for (i in 0 until 5) {
list.add("txt $i")
}
adapter.mData = list
adapter.notifyDataSetChanged() }
}
上面的做法是可以实现item之间的透明分割线,但是会有一个问题,就是最后一个item也有20dp的间隔。如下图,滚动到尽头时,发现最后一个item也有了分割线。
最后一个item也加上了分割线.png
3.有点麻烦的实现
如果我们想要去掉最后一个item的分割线,网上搜索到做法很多都是继承DividerItemDecoration
,再自己重写DividerItemDecoration
的onDraw
里的相关方法。
#DividerItemDecoration.java
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
if (parent.getLayoutManager() == null || mDivider == null) {
return;
}
if (mOrientation == VERTICAL) {
drawVertical(c, parent);
} else {
drawHorizontal(c, parent);
}
}
private void drawHorizontal(Canvas canvas, RecyclerView parent) {
...
//在这里遍历所有子view然后一个个画出分割线
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
parent.getLayoutManager().getDecoratedBoundsWithMargins(child, mBounds);
final int right = mBounds.right + Math.round(child.getTranslationX());
final int left = right - mDivider.getIntrinsicWidth();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
}
...
}
上面的遍历的时候,我们可以想到的就是,把for循环的childCount减1,这样最后一个item的分割线就不会画出来了。
private void drawHorizontal(Canvas canvas, RecyclerView parent) {
...
//在这里遍历所有子view然后一个个画出分割线
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount -1; i++) {//这里减了1,最后一个child的分割线就不画出来
final View child = parent.getChildAt(i);
parent.getLayoutManager().getDecoratedBoundsWithMargins(child, mBounds);
final int right = mBounds.right + Math.round(child.getTranslationX());
final int left = right - mDivider.getIntrinsicWidth();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
}
!!!然而,这种做法是没用的。你还是会看到最后一个item是有间隔的。为什么最后一个item还是会间隔呢?我们不是不让它画出来了吗。实际这个divider的的确确没有被画出来。改一下divider的颜色就可以看到,最后一个divider的确没有被画出来。
//间隔的xml实现,宽度20dp,颜色紫色
#item_divider_shape.xml
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<size android:width="20dp" />
<solid android:color="@android:color/holo_purple" />
</shape>
紫色divider.png
为什么最后多出来一些,因为这是多出来的宽度。是什么宽度呢?是这个RecyclerView需要的宽度,我们绘制之前肯定是先要测量确定大小。所以问题可能出在了测量的时候,测量把所有的item和所有的divider的宽度都计算进去了。
我们再看一下ItemDecoration
这个类里的方法,除了draw相关的,我们还可以看到一个方法getItemOffsets
,查看这个方法的调用,可以看到RecyclerView
里的一个方法调用到了getItemOffsets
。里面的代码是说什么呢,我们分析得出:它就是用传进来的child来获取ItemDecoration设置给这个child的大小。
#RecyclerView.java
Rect getItemDecorInsetsForChild(View child) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (!lp.mInsetsDirty) {
return lp.mDecorInsets;
}
if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
// changed/invalid items should not be updated until they are rebound.
return lp.mDecorInsets;
}
final Rect insets = lp.mDecorInsets;
insets.set(0, 0, 0, 0);
final int decorCount = mItemDecorations.size();
for (int i = 0; i < decorCount; i++) {
mTempRect.set(0, 0, 0, 0);
//这里可以获取到设置给ItemDecoration的drawable的大小。
mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState); insets.left += mTempRect.left;
insets.top += mTempRect.top;
insets.right += mTempRect.right;
insets.bottom += mTempRect.bottom;
}
lp.mInsetsDirty = false;
return insets;
}
再追踪这个getItemDecorInsetsForChild(View child)
,我们发现它有被RecyclerView的内部类LayoutManager
的measureChild
调用到。
我们知道父View肯定遍历每个子View进行测量的,所以最后一个item的divider的大小也是有算进去了。所以才会出现没有绘制divider,却出现了divider的情况。
#RecyclerView$LayoutManager.java
public void measureChild(@NonNull View child, int widthUsed, int heightUsed) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//这里拿到了ItemDecoration的大小
final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;
final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
getPaddingLeft() + getPaddingRight() + widthUsed, lp.width,
canScrollHorizontally());
final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
getPaddingTop() + getPaddingBottom() + heightUsed, lp.height,
canScrollVertically());
if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
child.measure(widthSpec, heightSpec);
}
}
到了这里,难道要重写RecyclerView的测量方法,那肯定不是。回到ItemDecoration
的getItemOffsets
,即然divider的大小需要调用它来计算得到,那么我们是否可以重写这个方法,答案是可以的。判断是不是最后一个item,是的话就设置为0,这样就解决了。
@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 {
int adapterPosition = parent.getChildAdapterPosition(view);
//判断是不是最后一个item,是的话就设置给outRect的数据都设置为0
if (adapterPosition == state.getItemCount() - 1) {
outRect.set(0, 0, 0, 0);
} else {
outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
}
}
}
image.png
可以看到最后一个item的间隔已经没有了。
但是我们真的需要把整个
DividerItemDecoration
的全部代码复制出来,再修改drawHorizontal
和getItemOffsets
这两个方法吗。有没有更简单的方法??当然有
4.简单的实现
由上面我们知道,如果在调用到getItemOffsets
这个方法时,如果不给最后一个item的设置divider的大小,那么最终测量出来的宽度就是不包含最后一个item的divider的宽度,即使最后一个item的divider被画出来,但它没地方显示啊。
#MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val adapter = ItemAdapter()
mRv.adapter = adapter
mRv.layoutManager = LinearLayoutManager(baseContext, RecyclerView.HORIZONTAL, false)
//这里我们重写getItemOffsets的实现即可
mRv.addItemDecoration(
object : DividerItemDecoration(
baseContext,
HORIZONTAL
) {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
//drawable就是我们的divider
drawable?.let {
when (parent.getChildAdapterPosition(view)) {
state.itemCount - 1 -> {//是不是最后一个,是的话设置divider的宽度为0
outRect.set(0, 0, 0, 0)
}
else -> {
outRect.set(0, 0, it.intrinsicWidth, 0)
}
}
}
}
}.apply {
setDrawable(resources.getDrawable(R.drawable.item_divider_shape))
})
val list = mutableListOf<String>()
for (i in 0 until 5) {
list.add("txt $i")
}
adapter.mData = list
adapter.notifyDataSetChanged()
}
}
5.最后
记一下笔记,如果有错误帮忙提出,谢谢大家。
网友评论