给RecyclerView的item优雅的加上等宽间距

7,023 阅读6分钟

前言

作为Android中常用的控件——RecyclerView 。我们在使用 RecyclerView 的时候,都会使用各种各样的 LayoutManager(你如果不使用 LayoutManager 的话,界面上不会显示任何内容) ,但是一般情况下是如下的效果,没有任何间距,效果不太好看,看起来比较拥挤。

这个时候有的同学可能就想到了一个办法,我给 item 加上 margin 不就行了吗?是的,你可以这么做。不过,我们还有更好的做法——使用 addItemDecoration 方法来实现。

What's is a ItemDecoration?

官方链接

官方描述:

Add an RecyclerView.ItemDecoration to this RecyclerView. Item decorations can affect both measurement and drawing of individual item views.

Item decorations are ordered. Decorations placed earlier in the list will be run/queried/drawn first for their effects on item views. Padding added to views will be nested; a padding added by an earlier decoration will mean further item decorations in the list will be asked to draw/pad within the previous decoration's given area.

译:

给这个 RecyclerView 添加一个 RecyclerView.ItemDecoration 。项目装饰可以影响单个项目视图的测量和绘制。

项目装饰是有顺序的。在列表中较早放置的装饰将首先运行/查询/绘制它们对项目视图的影响。添加到视图中的填充物将被嵌套;由早期装饰添加的填充物将意味着列表中的其他项目装饰将被要求在先前装饰的给定区域内绘制/填充。


简单点来说就是,我们可以通过复写 ItemDecoration 中的方法,来实现对 RecyclerView Item的装饰,正如 ItemDecoration 的名字一样:Item 装饰。

RecyclerView.ItemDecoration

RecyclerView.ItemDecoration 是一个抽象类,我们使用时通常是直接通过匿名内部类的方式去new出来,然后重写需要实现的方法。

ItemDecoration 中有以下三个方法。(准确来说是六个,因为有一半的方法被打上了 @Deprecated 注解,不推荐使用了)

onDraw:

		/**
         * Draw any appropriate decorations into the Canvas supplied to the RecyclerView.
         * Any content drawn by this method will be drawn before the item views are drawn,
         * and will thus appear underneath the views.
         *
         * @param c Canvas to draw into
         * @param parent RecyclerView this ItemDecoration is drawing into
         * @param state The current state of RecyclerView
         */
        public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull State state) {
            onDraw(c, parent);
        }

onDrawOver:

		/**
         * Draw any appropriate decorations into the Canvas supplied to the RecyclerView.
         * Any content drawn by this method will be drawn after the item views are drawn
         * and will thus appear over the views.
         *
         * @param c Canvas to draw into
         * @param parent RecyclerView this ItemDecoration is drawing into
         * @param state The current state of RecyclerView.
         */
        public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent,
                @NonNull State state) {
            onDrawOver(c, parent);
        }

getItemOffsets:

    /**
     * Retrieve any offsets for the given item. Each field of <code>outRect</code> specifies
     * the number of pixels that the item view should be inset by, similar to padding or margin.
     * The default implementation sets the bounds of outRect to 0 and returns.
     *
     * <p>
     * If this ItemDecoration does not affect the positioning of item views, it should set
     * all four fields of <code>outRect</code> (left, top, right, bottom) to zero
     * before returning.
     *
     * <p>
     * If you need to access Adapter for additional data, you can call
     * {@link RecyclerView#getChildAdapterPosition(View)} to get the adapter position of the
     * View.
     *
     * @param outRect Rect to receive the output.
     * @param view    The child view to decorate
     * @param parent  RecyclerView this ItemDecoration is decorating
     * @param state   The current state of RecyclerView.
     */
    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
            @NonNull RecyclerView parent, @NonNull State state) {
        getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
                parent);
    }
}

实现思路

首先,我们常用的 LayoutManager 就是 LinearLayoutManagerGridLayoutManager 了,所以我们分成两种方案去实现。

LinearLayoutManager

纵向(VERTICAL)

  • 第一个 item 的顶部和左右两边均设置两个单位的间距,底部设置一个单位的间距。
  • 最后一个 item 的底部和左右两边均设置两个单位的间距,顶部设置一个单位的间距。
  • 其它的 item 上下设置均设置一个单位的间距,左右两边均设置两个单位的间距。

横向(HORIZONTAL)

  • 第一个 item 的上下和左边均设置两个单位的间距,右边设置一个单位的间距。
  • 最后一个 item 的上下和右两边均设置两个单位的间距,左边设置一个单位的间距。
  • 其它的 item 上下设置均设置两个单位的间距,左右两边均设置一个单位的间距。

GridLayoutManager

小提示:GridLayoutManager 的设置比较特殊一点,如果 spanCount 设置为 1 ,那么其实就是 LinearLayoutManager 的效果了,所以我们直接使用 LinearLayoutManager 的设置即可。

纵向(VERTICAL)

  • 最左边的一列(不是最后一行),上边和左边均设置两个单位的间距,右边均设置一个单位的间距。
  • 最右边的一列(不是最后一行),上边和右边均设置两个单位的间距,左边均设置一个单位的间距。
  • 中间的列(不是最后一行),上边设置两个单位的间距,左右两边均设置一个单位的间距。
  • 最后一行的最左边那一列,顶部和底部均设置两个单位的间距,右边设置一个单位的间距。
  • 最后一行的最右边那一列,顶部和底部均设置两个单位的间距,左边设置一个单位的间距。
  • 最后一行中间的列,顶部和底部均设置两个单位的间距,左右两边均设置一个单位的间距。

提问:那么如果只有一行呢?emmm,按照默认情况处理好了🤐🤐

横向(HORIZONTAL)

暂不支持。

具体实现

我使用的是 Kotlin 的扩展函数实现的,同学们如果用 Java 的话,请自行根据笔者的思路去写哈

RecyclerView.kt

import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView

/**
 * {@link ItemDecoration#getItemOffsets(outRect: Rect,view: View,parent: RecyclerView)} or
 * {@link ItemDecoration#getItemOffsets(outRect: Rect,view: View,parent: RecyclerView,state: RecyclerView.State)}.
 * 均分 LinearLayoutManager 间距的便捷方法
 */
fun equilibriumAssignmentOfLinear(
    unit: Int,
    outRect: Rect,
    view: View,
    parent: RecyclerView
) {
    // item 的个数
    val itemCount = parent.getItemCount()
    // 当前 item 的 position
    val itemPosition = parent.getChildAdapterPosition(view)
    val layoutManager = parent.checkLinearLayoutManager() ?: return
    // 获取 LinearLayoutManager 的布局方向
    val orientation = layoutManager.orientation
    // 遍历所有 item
    for (index in 0..itemCount) {
        when (itemPosition) {
            // 第一行/列
            0 -> {
                if (orientation == RecyclerView.VERTICAL) {
                    // 第一行/列 && VERTICAL 布局方式 -> 对item的底部特殊处理
                    outRect.top = unit * 2
                    outRect.bottom = unit
                    outRect.left = unit * 2
                    outRect.right = unit * 2
                } else {
                    // 第一行/列 && HORIZONTAL 布局方式 -> 对item的右边特殊处理
                    outRect.top = unit * 2
                    outRect.bottom = unit * 2
                    outRect.left = unit * 2
                    outRect.right = unit
                }
            }
            // 最后一行/列
            itemCount - 1 -> {
                if (orientation == RecyclerView.VERTICAL) {
                    // 最后一行/列 && VERTICAL 布局方式 -> 对item的顶部特殊处理
                    outRect.top = unit
                    outRect.bottom = unit * 2
                    outRect.left = unit * 2
                    outRect.right = unit * 2
                } else {
                    // 最后一行/列 && HORIZONTAL 布局方式 -> 对item的左边特殊处理
                    outRect.top = unit * 2
                    outRect.bottom = unit * 2
                    outRect.left = unit
                    outRect.right = unit * 2
                }
            }
            // 中间的行/列
            else -> {
                if (orientation == RecyclerView.VERTICAL) {
                    // 中间的行/列 && VERTICAL 布局方式 -> 对item的顶部和底部特殊处理
                    outRect.top = unit
                    outRect.bottom = unit
                    outRect.left = unit * 2
                    outRect.right = unit * 2
                } else {
                    // 中间的行/列 && HORIZONTAL 布局方式 -> 对item的左边和右边特殊处理
                    outRect.top = unit * 2
                    outRect.bottom = unit * 2
                    outRect.left = unit
                    outRect.right = unit
                }
            }
        }
    }
}

/**
 * {@link ItemDecoration#getItemOffsets(outRect: Rect,view: View,parent: RecyclerView)} or
 * {@link ItemDecoration#getItemOffsets(outRect: Rect,view: View,parent: RecyclerView,state: RecyclerView.State)}.
 * 均分 GridLayoutManager 间距的便捷方法
 */
fun equilibriumAssignmentOfGrid(
    unit: Int,
    outRect: Rect,
    view: View,
    parent: RecyclerView
) {
    // item 的个数
    val itemCount = parent.getItemCount()
    // 网格布局的跨度数
    val spanCount = parent.getSpanCount()
    // 当前 item 的 position
    val itemPosition = parent.getChildAdapterPosition(view)
    val layoutManager = parent.checkGridLayoutManager() ?: return
    if (spanCount < 2) {
        equilibriumAssignmentOfLinear(view = view, unit = unit, parent = parent, outRect = outRect)
        return
    }
    // 获取 GridLayoutManager 的布局方向
    val orientation = layoutManager.orientation
    if (orientation == RecyclerView.HORIZONTAL) {
        // 暂不支持横向布局的 GridLayoutManager
        throw UnsupportedOperationException("You can’t set a horizontal grid layout because we don’t support!")
    }
    // 遍历所有 item
    for (index in 0..itemCount) {
        when {
            // 最左边的那一列
            itemPosition % spanCount == 0 -> {
                outRect.left = unit * 2
                outRect.right = unit
            }
            // 最右边的那一列
            (itemPosition - (spanCount - 1)) % spanCount == 0 -> {
                outRect.left = unit
                outRect.right = unit * 2
            }
            // 中间的列(可能有多列)
            else -> {
                outRect.left = unit
                outRect.right = unit
            }
        }
        outRect.top = unit * 2
        // 判断是否为最后一行,最后一行单独添加底部的间距
        if (itemPosition in (itemCount - spanCount) until itemCount) {
            outRect.bottom = unit * 2
        }
    }
}

/**
 * 获取 spanCount
 * 注:此方法只针对设置 LayoutManager 为 GridLayoutManager 的 RecyclerView 生效
 */
fun RecyclerView.getSpanCount(): Int {
    val layoutManager = checkGridLayoutManager() ?: return 0
    return layoutManager.spanCount
}

/**
 * 返回绑定到父 RecyclerView 的适配器中的项目数
 */
fun RecyclerView.getItemCount(): Int {
    val layoutManager = layoutManager ?: return 0
    return layoutManager.itemCount
}

/**
 * 检查 RecyclerView 设置的 LinearLayoutManager
 */
private fun RecyclerView.checkLinearLayoutManager(): LinearLayoutManager? {
    val layoutManager =
        layoutManager ?: return null
    if (layoutManager !is LinearLayoutManager) {
        throw IllegalStateException("Make sure you are using the LinearLayoutManager!")
    }
    return layoutManager
}

/**
 * 检查 RecyclerView 设置的 GridLayoutManager
 */
private fun RecyclerView.checkGridLayoutManager(): GridLayoutManager? {
    val layoutManager =
        layoutManager ?: return null
    if (layoutManager !is GridLayoutManager) {
        throw IllegalStateException("Make sure you are using the GridLayoutManager!")
    }
    return layoutManager
}

食用方式

	recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {

        // 单位间距(实际间距的一半)
        private val unit = 4.dp

        override fun getItemOffsets(
            outRect: Rect,
            view: View,
            parent: RecyclerView,
            state: RecyclerView.State
        ) {
            super.getItemOffsets(outRect, view, parent, state)
            equilibriumAssignmentOfGrid(unit, outRect, view, parent)
            // equilibriumAssignmentOfLinear(unit, outRect, view, parent)
        }
    })

实现效果

LinearLayoutManager

GridLayoutManager

首次在掘金发文,欢迎各位点赞、评论、打赏+关注啦,如有错误之处,请各位批评指正

笔者的更多文章请移步我的阳光沙滩个人主页
打赏作者