仿Excel表格左边和顶部悬浮横向纵向滑动

package com.lphtsccft.zhangle.market.ranking.view

import androidx.recyclerview.widget.RecyclerView

class FloatingHeaderAndColumnLayoutManager(val columnCount: Int = 10) : RecyclerView.LayoutManager() {

    private var horizontalScrollOffset = 0
    private var verticalScrollOffset = 0
    private var currentScrollDirection: ScrollDirection? = null

    private var totalWidth = 0
    private var totalHeight = 0
    private var mRowCount = 0

    private enum class ScrollDirection { HORIZONTAL, VERTICAL }

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

    override fun canScrollHorizontally(): Boolean = true

    override fun canScrollVertically(): Boolean = true

    override fun scrollHorizontallyBy(dx: Int, recycler: RecyclerView.Recycler, state: RecyclerView.State): Int {
        if (currentScrollDirection == null) currentScrollDirection = ScrollDirection.HORIZONTAL
        if (currentScrollDirection != ScrollDirection.HORIZONTAL || mWidthDataList.maxOf { it.value } <= width) return 0

        val scrollBy = calculateHorizontalScroll(dx)
        offsetChildrenHorizontal(-scrollBy)
        detachAndScrapAttachedViews(recycler)
        layoutVisibleCells(recycler)
        return scrollBy
    }


    override fun scrollVerticallyBy(dy: Int, recycler: RecyclerView.Recycler, state: RecyclerView.State): Int {
        if (currentScrollDirection == null) currentScrollDirection = ScrollDirection.VERTICAL
        if (currentScrollDirection != ScrollDirection.VERTICAL || mHeightDataList.maxOf { it.value } <= height) return 0

        val scrollBy = calculateVerticalScroll(dy)
        detachAndScrapAttachedViews(recycler)
        offsetChildrenVertical(-scrollBy)
        layoutVisibleCells(recycler)
        return scrollBy
    }

    override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
        if (itemCount == 0 || state.isPreLayout) return

        detachAndScrapAttachedViews(recycler)

        // 计算内容宽高
        calculateDimensions(recycler)

        // 布局可见单元格
        layoutVisibleCells(recycler)
    }

    override fun onScrollStateChanged(state: Int) {
        super.onScrollStateChanged(state)
        if (state == RecyclerView.SCROLL_STATE_IDLE) {
            currentScrollDirection = null // 滚动停止后重置方向锁
        }
    }

    private fun calculateDimensions(recycler: RecyclerView.Recycler) {
        mHeightDataList.clear()
        mWidthDataList.clear()
        mRowCount = if (itemCount % columnCount == 0) itemCount / columnCount else (itemCount / columnCount) + 1
        for (i in 0 until columnCount) {
            val view = recycler.getViewForPosition(i)
            measureChildWithMargins(view, 0, 0)
            totalWidth += getDecoratedMeasuredWidth(view)
            mWidthDataList.add(Bean(i, totalWidth))
        }

        for (i in 0 until mRowCount) {
            val view = recycler.getViewForPosition(i * columnCount)
            measureChildWithMargins(view, 0, 0)
            totalHeight += getDecoratedMeasuredHeight(view)
            mHeightDataList.add(Bean(i, totalHeight))
        }
    }

    private val mWidthDataList: MutableList<Bean> = mutableListOf()
    private val mHeightDataList: MutableList<Bean> = mutableListOf()

    data class Bean(val index: Int, val value: Int)

    private fun layoutVisibleCells(recycler: RecyclerView.Recycler) {
        val visibleLeft = horizontalScrollOffset
        val visibleTop = verticalScrollOffset
        val visibleRight = visibleLeft + width
        val visibleBottom = visibleTop + height


        val startColumn = mWidthDataList.find { it.value >= visibleLeft }?.index ?: 0
        val endColumn = Math.min((mWidthDataList.find { it.value >= visibleRight }?.index ?: columnCount) + 1, columnCount)

        val startRow = mHeightDataList.find { it.value >= visibleTop }?.index ?: 0
        val endRow = Math.min((mHeightDataList.find { it.value >= visibleBottom }?.index ?: mRowCount) + 1, mRowCount)


        // 布局其他单元格
        for (row in startRow until endRow) {
            for (column in startColumn until endColumn) {
                val position = row * columnCount + column
                if (position >= itemCount) continue
                if (row == 0 || column == 0) continue // 跳过表头和左列

                val view = recycler.getViewForPosition(position)
                addView(view)
                measureChildWithMargins(view, 0, 0)
                val height = getDecoratedMeasuredHeight(view)
                val width = getDecoratedMeasuredWidth(view)
                val left = mWidthDataList[column - 1].value - horizontalScrollOffset
                val top = mHeightDataList[row - 1].value - verticalScrollOffset
                layoutDecorated(view, left, top, left + width, top + height)
            }
        }

        var cStart = 0
        // 布局悬浮表头(第一行)
        for (column in startColumn until endColumn) {
            if (column >= itemCount) continue

            val view = recycler.getViewForPosition(column)
            addView(view)
            measureChildWithMargins(view, 0, 0)
            val height = getDecoratedMeasuredHeight(view)
            val width = getDecoratedMeasuredWidth(view)
            val left = cStart - horizontalScrollOffset
            layoutDecorated(view, left, 0, left + width, height)
            cStart = mWidthDataList[column].value
        }

        var rStart = 0
        // 布局悬浮左列(第一列)
        for (row in startRow until endRow) {
            val position = row * columnCount // 每行的第一列单元格位置
            if (position >= itemCount) continue

            val view = recycler.getViewForPosition(position)
            addView(view)
            measureChildWithMargins(view, 0, 0)
            val height = getDecoratedMeasuredHeight(view)
            val width = getDecoratedMeasuredWidth(view)
            val top = rStart - verticalScrollOffset
            layoutDecorated(view, 0, top, width, top + height)
            rStart = mHeightDataList[row].value
        }

        // 处理坐标(0,0)
        val view = recycler.getViewForPosition(0)
        addView(view)
        measureChildWithMargins(view, 0, 0)
        val height = getDecoratedMeasuredHeight(view)
        val width = getDecoratedMeasuredWidth(view)
        layoutDecorated(view, 0, 0, width, height)
    }

    private fun calculateHorizontalScroll(dx: Int): Int {
        val maxScroll = totalWidth - width
        val scrollBy = when {
            horizontalScrollOffset + dx < 0 -> -horizontalScrollOffset
            horizontalScrollOffset + dx > maxScroll -> maxScroll - horizontalScrollOffset
            else -> dx
        }
        horizontalScrollOffset += scrollBy
        return scrollBy
    }

    private fun calculateVerticalScroll(dy: Int): Int {
        val maxScroll = totalHeight - height
        val scrollBy = when {
            verticalScrollOffset + dy < 0 -> -verticalScrollOffset
            verticalScrollOffset + dy > maxScroll -> maxScroll - verticalScrollOffset
            else -> dy
        }
        verticalScrollOffset += scrollBy
        return scrollBy
    }
}

表格

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值