Home 自定义RecyclerView LayoutManager
Post
Cancel

自定义RecyclerView LayoutManager

LayoutManger使用

LayoutManager 是配合 RecyclerView 使用的,它主要的作用是控制 RecyclerView 子item的显示布局。比如自带的实现有:

  • LinearLayoutManager。支持X轴线性布局或者Y轴线性布局
  • GridLayoutManager。网格布局
  • StaggeredGridLayoutManager。流式布局。

使用起来也是非常的简单:

1
recyclerView.layoutManager = LinearLayoutManager()

设计成插拔式,将数据与布局完美的拆分开来,可以说是非常流弊了。


自定义LayoutManager,实现简单的垂直布局

LayoutManagerRecyclerView 的静态内部抽象类,自定义LayoutManager需要继承它,并要复写其中的几个方法:

  • generateDefaultLayoutParams()。 设置默认的布局参数,主要是给子item使用的。
  • onLayoutChildren()。布局子item,当recyclerView调用notify的时候,会调用该方法。
  • canScrollVertically()。 能否垂直滚动。因为是垂直布局,所以要复写该方法。
  • scrollVerticallyBy()。垂直方向滚动的距离(与上一个位置的距离差), 需要在这里面重新布局界面。

调用流程如下:

layout流程图

效果图:

效果图

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import android.graphics.Rect
import android.support.v7.widget.RecyclerView
import android.util.Log

class DemoLayoutManager : RecyclerView.LayoutManager() {

    val TAG = javaClass.simpleName
    //滑动的总距离
    var mScrollOffset = 0 
    //item高度和
    var totalHeight = 0 
    var allItemRects = mutableMapOf<Int, Rect>()

    override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
        return RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT,
                RecyclerView.LayoutParams.WRAP_CONTENT)
    }

    override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
        if (itemCount == 0 || state.isPreLayout) {
            return
        }
		//layout item之前,需要detach所有的view
        detachAndScrapAttachedViews(recycler)
        //计算item总高度
        (0 until itemCount).map { recycler.getViewForPosition(it) }
                .forEachIndexed { index, indexedValue ->
                    measureChildWithMargins(indexedValue, 0, 0)
                    val width = getDecoratedMeasuredWidth(indexedValue)
                    val height = (width * 0.5F).toInt()
                    allItemRects[index] = Rect(0, totalHeight, width, totalHeight + height)
                    Log.d(TAG, allItemRects[index].toString())
                    totalHeight += height
                }
        //layout item
        doLayout(recycler, state)
    }

    private fun doLayout(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
        val displayRect = Rect(0, mScrollOffset, width, height + mScrollOffset)
        (0 until itemCount).filter { Rect.intersects(displayRect, allItemRects[it]) }
                .forEach {
                    val view = recycler.getViewForPosition(it)
                    measureChildWithMargins(view, 0, 0)
                    addView(view)
                    //在手动measure完view的大小之后,需要调用 calculateItemDecorationsForChild ,计算出item decorator的大小,并保存在lp.mDecorInsets中
                    //因为后续需要用到这个值, layoutDecorated 方法便需要用到
                    calculateItemDecorationsForChild(view, Rect())
                    val rect: Rect = allItemRects[it]!!
                    layoutDecoratedWithMargins(view, rect.left, rect.top - mScrollOffset, rect.right,rect.bottom - mScrollOffset)
                }
    }


    override fun canScrollVertically(): Boolean {
        return true
    }

    override fun scrollVerticallyBy(dy: Int, recycler: RecyclerView.Recycler, state: RecyclerView.State): Int {
        //layout item之前,需要detach所有的view
        detachAndScrapAttachedViews(recycler)
        val preScrollOffset = mScrollOffset
        mScrollOffset = Math.min(Math.max(0, mScrollOffset + dy), totalHeight - height)
        offsetChildrenVertical(preScrollOffset - mScrollOffset)
        //layout item
        doLayout(recycler, state)
        return mScrollOffset - preScrollOffset
    }

}


卡牌叠加式布局实现

效果图:

效果图

实现思路:

每个item在屏幕中自下而上显示,周期 与 item从底部显示开始滑动长度为 totalItemInScreen * itemHeight相同。

而item开始scale的点是在scrollOffset滑动了itemHeight长度之后。

那item在屏幕中显示的scale与top,便可以与scrollOffset这个变量之间,存在关系了。


完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import android.graphics.Rect
import android.support.v7.widget.RecyclerView
import android.util.Log

class Demo2LayoutManager : RecyclerView.LayoutManager() {

    val TAG = javaClass.simpleName
    var mScrollOffset = 0 //滑动总长度
    var itemWidth = 0
    var itemHeight = 0
    val totalItemInScreen = 7 //一个屏幕中item的个数
    val minItemScaled = 0.7F //item最多可以scale的大小


    override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
        Log.d(TAG, "generateDefaultLayoutParams")
        return RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT,
                RecyclerView.LayoutParams.WRAP_CONTENT)
    }

    override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
        Log.d(TAG, "onLayoutChildren")

        if (itemCount == 0 || state.isPreLayout) {
            return
        }

        detachAndScrapAttachedViews(recycler)
        itemWidth = (width * 0.9F).toInt()
        itemHeight = (itemWidth * 1.6F).toInt()
        doLayout(recycler, state)
    }

    private fun doLayout(recycler: RecyclerView.Recycler, state: RecyclerView.State) {

        val bottomVisiblePosition = Math.ceil(mScrollOffset / itemHeight.toDouble()).toInt()
        val bottomViewHeight = if (mScrollOffset % itemHeight == 0) itemHeight else mScrollOffset % itemHeight

        val viewLayoutInfoList = mutableListOf<ViewLayoutInfo>()

        viewLayoutInfoList.add(ViewLayoutInfo(bottomVisiblePosition, 1.0F, getRecyclerViewHeight() - bottomViewHeight))

        if (bottomVisiblePosition > 0) {
            IntRange(0, bottomVisiblePosition - 1).reversed().forEach {

                val offset = (bottomVisiblePosition - 1 - it) * itemHeight + bottomViewHeight
                val rate = 1 - offset / (itemHeight * (totalItemInScreen - 1F))
                val scale = (1 - minItemScaled) * rate + minItemScaled
                val top = (getRecyclerViewHeight() - itemHeight) * rate * rate
                viewLayoutInfoList.add(ViewLayoutInfo(it, scale, top.toInt()))

                if (rate <= 0F) return@forEach
            }
        }

        viewLayoutInfoList.forEach {
            val view = recycler.getViewForPosition(it.position)
            measureChildWithMargins(view, 0, 0)
            addView(view, 0)
            calculateItemDecorationsForChild(view, Rect())
            val left = (getRecyclerViewWidth() / 2 - itemWidth * it.scale / 2).toInt()
            val right = (left + itemWidth * it.scale).toInt()
            val top = it.top
            val bottom = (top + itemHeight * it.scale).toInt()
            layoutDecoratedWithMargins(view, left, top, right, bottom)
        }

    }

    override fun canScrollVertically(): Boolean {
        Log.d(TAG, "canScrollVertically")
        return true
    }

    override fun scrollVerticallyBy(dy: Int, recycler: RecyclerView.Recycler, state: RecyclerView.State): Int {
        Log.d(TAG, "scrollVerticallyBy")
        detachAndScrapAttachedViews(recycler)
        val preScrollOffset = mScrollOffset
        mScrollOffset = Math.min(Math.max(0, mScrollOffset + dy), itemHeight * (itemCount - 1))
        offsetChildrenVertical(preScrollOffset - mScrollOffset)
        doLayout(recycler, state)
        return mScrollOffset - preScrollOffset
    }

    private fun getRecyclerViewWidth() = width

    private fun getRecyclerViewHeight() = height
}

ViewLayoutInfo 结构如下:

1
2
3
data class ViewLayoutInfo(var position: Int, //item position
                          var scale: Float, //item scale的大小
                          var top: Int) //item top的y轴坐标
This post is licensed under CC BY 4.0 by the author.