Android 复杂表格的解决方案

关键词:自定义View、表格、合并单元格、固定吸附、粘性吸附


背景:

表格展示作为常见需求,从 GridView,到 RecyclerView + GridLayoutManager,都是作为基本控件出现的。但是,产品要的从来不止系统能够支持的,比如合并单元格、行列吸附等效果。而网络上存在的仿excel表格控件,功能复杂,不便使用。因此我们来愉快的造轮子吧!


效果示意:
合并单元格

124_1705071364

第二行、第二列、第五行、第五列都是固定吸附
第三行、第三列都是粘性吸附


功能简介:

  1. 同时支持横纵方向合并单元格展示
  2. 支持任意数量的任意行、列的固定吸附效果
  3. 数据量少时支持均分,数据量多时支持滑动
  4. 行宽、列高支持自适应

设计思路:

首先,表格只负责单元格位置,不应该负责单元格内具体展示内容,否则业务迭代时需要深入表格控件中修改,非常缺乏通用型。因此引入 adapter,表格控件只需要根据adapter返回的视图View,去计算位置。

其次,如果要嵌套到滑动布局中,那么表格就不知道自己什么时候应该吸附了,就需要自己处理下滑动效果,记录每次滑动距离,onLayout 的时候把距离左边和上边的距离减掉横纵滑动的距离就OK

然后,对于行列的两种吸附效果,实际上就是在 onLayout 的时候再加一点判断,判断下实际展示位置是否满足吸附条件,满足就要按照吸附时坐标展示了。

最后,为了保证吸附行列在吸附的时候不能被其他单元格覆盖,就需要简单控制下 childView 的顺序。哦,吸附行列的交叉点的展示优先级和非交叉点也不一样,这点需要注意。

哦,差点忘了,为了自适应,onMeasure时需要计算两遍。

那么,这个表格控件就可以开撸了,不复杂,对吧~


当然还有点小坑没能解决

  1. 每个单元格需要设计点击监听,不然 ZrTableView 拿不到 onTouch 事件
  2. 每个单元格需要单独设置背景,不然会透视
  3. 吸附信息和合并单元格信息设置完后需要手动 adapter.notifyDataSetChanged(),不然展示时上下层级错乱。

直接上代码:

package com.elee.uidemo

import android.content.Context
import android.graphics.Canvas
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.view.ViewGroup
import androidx.core.view.children
import kotlin.math.absoluteValue

/**
 * 表格view
 */
class ZrTableView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : ViewGroup(context, attrs, defStyleAttr) {

    companion object {
        const val SCROLL_DIRECTION_VERTICAL = 0
        const val SCROLL_DIRECTION_HORIZONTAL = 1
    }

    init {
        setWillNotDraw(false)
        isNestedScrollingEnabled = true
//        isFocusable = true
    }

    private val mTouchSlop: Int by lazy { ViewConfiguration.get(context).scaledTouchSlop }

    private val mManager = CellsManager()

    private val mHelper = LayoutHelper()

    private var horizontalScrollEnable = false
    private var verticalScrollEnable = false

    private var horizontalScrollSpace: Int = 0
    private var verticalScrollSpace: Int = 0

    private var mIsBeingDragged: Boolean = false
    private val mScrollConsumed = IntArray(2)
    private val mWindowOffSet = IntArray(2)

    var adapter: Adapter? = null
        set(value) {
            field = value
            updateAllView()

            field?.apply {
                updateListener = { column, row ->
                    if (column == -1 || row == -1) {
                        // 全部更新
                        updateAllView()
                    } else {
                        // 单个 item 更新,不改变宽度
                        getCellView(row, column)?.let {
                            bindData(
                                it,
                                row,
                                column,
                                getCellType(column, row)
                            )
                        }
                    }
                }

            }
        }

    /**
     * 添加单个合并单元格信息
     */
    fun addSpanInfo(spanInfo: SpanCellItemInfo) {
        mHelper.spanCellInfo.add(spanInfo)
    }

    /**
     * 设置合并单元格信息
     */
    fun setSpanList(spanList: MutableList<SpanCellItemInfo>) {
        mHelper.spanCellInfo.clear()
        mHelper.spanCellInfo.addAll(spanList)
    }

    /**
     * 设置固定列
     */
    fun setStableColumnList(list: MutableList<Int>) {
        mHelper.stableColumnIndex.clear()
        mHelper.stableColumnIndex.addAll(list)
        mHelper.stableColumnIndex.sort()
    }

    /**
     * 设置固定行
     */
    fun setStableRowList(list: MutableList<Int>) {
        mHelper.stableRowIndex.clear()
        mHelper.stableRowIndex.addAll(list)
        mHelper.stableRowIndex.sort()
    }

    /**
     * 设置粘性吸附列
     */
    fun setStickyColumnList(list: MutableList<Int>) {
        mHelper.stickyColumnIndex.clear()
        mHelper.stickyColumnIndex.addAll(list)
        mHelper.stickyColumnIndex.sort()
    }

    /**
     * 设置粘性吸附行
     */
    fun setStickyRowList(list: MutableList<Int>) {
        mHelper.stickyRowIndex.clear()
        mHelper.stickyRowIndex.addAll(list)
        mHelper.stickyRowIndex.sort()
    }

    fun scrollStable(column: Int = -1, row: Int = -1) {
        if (column in 0 until mManager.columnCount && horizontalScrollEnable) {
            horizontalScrollSpace = getLeftSpicalSpace(column) - mManager.getLeftSpace(column)
        }
        if (row in 0 until mManager.rowCount && verticalScrollEnable) {
            verticalScrollSpace = getTopSpicalSpace(row) - mManager.getTopSpace(row)
        }
        requestLayout()
    }

    private fun updateAllView() {
        adapter?.let {

            removeAllViews()

            mManager.reset()
            mManager.columnCount = it.getColumnCount()
            mManager.rowCount = it.getRowCount()

            // 第一层添加普通view
            for (r in 0 until mManager.rowCount) {
                for (c in 0 until mManager.columnCount) {
                    if (mHelper.showCell(c, r)) {
                        if (!mHelper.isStickyCell(c, r) && !mHelper.isStableCell(c, r)) {
                            addView(it.createCellView(this, c, r, it.getCellType(c, r)).apply {
                                layoutParams = LayoutParams().apply {
                                    column = c
                                    row = r
                                }
                            })
                        }
                    }
                }
            }

            // 第二层添加粘性吸附view非交叉点
            for (r in 0 until mManager.rowCount) {
                for (c in 0 until mManager.columnCount) {
                    if (mHelper.showCell(c, r) && mHelper.isStickyCell(c, r)) {
                        if ((!mHelper.stickyRowIndex.contains(r) || !mHelper.stickyColumnIndex.contains(
                                c
                            ) && !mHelper.stableRowIndex.contains(r) && !mHelper.stableColumnIndex.contains(
                                c
                            ))
                        ) {
                            addView(it.createCellView(this, c, r, it.getCellType(c, r)).apply {
                                layoutParams = LayoutParams().apply {
                                    column = c
                                    row = r
                                }
                            })
                        }
                    }
                }
            }

            // 第三层添加粘性吸附view交叉点
            for (r in mHelper.stickyRowIndex) {
                for (c in mHelper.stickyColumnIndex) {
                    if (mHelper.showCell(c, r) && mHelper.isStickyCell(c, r)) {
                        if (mHelper.stickyRowIndex.contains(r) && mHelper.stickyColumnIndex.contains(
                                c
                            )
                        ) {
                            addView(it.createCellView(this, c, r, it.getCellType(c, r)).apply {
                                layoutParams = LayoutParams().apply {
                                    column = c
                                    row = r
                                }
                            })
                        }
                    }
                }
            }

            // 第四层添加固定吸附view非交叉点
            for (r in 0 until mManager.rowCount) {
                for (c in 0 until mManager.columnCount) {
                    if (mHelper.showCell(c, r) && mHelper.isStableCell(c, r)) {
                        if (!mHelper.stableRowIndex.contains(r) || !mHelper.stableColumnIndex.contains(
                                c
                            )
                        ) {
                            addView(it.createCellView(this, c, r, it.getCellType(c, r)).apply {
                                layoutParams = LayoutParams().apply {
                                    column = c
                                    row = r
                                }
                            })
                        }
                    }
                }
            }

            // 第五层添加固定吸附与粘性吸附view交叉点
            for (r in 0 until mManager.rowCount) {
                for (c in 0 until mManager.columnCount) {
                    if (mHelper.showCell(c, r)) {
                        if (mHelper.stableRowIndex.contains(r) && mHelper.stickyColumnIndex.contains(
                                c
                            )
                        ) {
                            addView(it.createCellView(this, c, r, it.getCellType(c, r)).apply {
                                layoutParams = LayoutParams().apply {
                                    column = c
                                    row = r
                                }
                            })
                        }
                        if (mHelper.stickyRowIndex.contains(r) && mHelper.stableColumnIndex.contains(
                                c
                            )
                        ) {
                            addView(it.createCellView(this, c, r, it.getCellType(c, r)).apply {
                                layoutParams = LayoutParams().apply {
                                    column = c
                                    row = r
                                }
                            })
                        }
                    }
                }
            }

            // 第六层添加固定吸附view交叉点
            for (r in mHelper.stableRowIndex) {
                for (c in mHelper.stableColumnIndex) {
                    if (mHelper.showCell(c, r) && mHelper.isStableCell(c, r)) {
                        if (mHelper.stableRowIndex.contains(r) && mHelper.stableColumnIndex.contains(
                                c
                            )
                        ) {
                            addView(it.createCellView(this, c, r, it.getCellType(c, r)).apply {
                                layoutParams = LayoutParams().apply {
                                    column = c
                                    row = r
                                }
                            })
                        }
                    }
                }
            }

            children.forEach { v ->
                if (v.layoutParams is LayoutParams) {
                    (v.layoutParams as LayoutParams)?.apply {
                        it.bindData(v, column, row, it.getCellType(column, row))
                    }
                }
            }
        }
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {

        // measure 计算默认宽高
        children.forEach {
            it.measure(
                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
            )
        }

        if (mManager.columnCount == 0 || mManager.rowCount == 0) {
            setMeasuredDimension(0, 0)
            return
        }

        // 确认每行最大高度和每列最大宽度
        var maxSize: Int = 0
        mManager.rowHeightSizeList.clear()
        mManager.columnWidthSizeList.clear()
        for (c in 0 until mManager.columnCount) {
            maxSize = 0
            for (r in 0 until mManager.rowCount) {
                if (!mHelper.isSpanCell(c, r)) {
                    getCellView(r, c)?.let {
                        maxSize = maxSize.coerceAtLeast(it.measuredWidth)
                    }
                }
            }
            mManager.columnWidthSizeList.add(maxSize)
        }
        for (r in 0 until mManager.rowCount) {
            maxSize = 0
            for (c in 0 until mManager.columnCount) {
                if (!mHelper.isSpanCell(c, r)) {
                    getCellView(r, c)?.let {
                        maxSize = maxSize.coerceAtLeast(it.measuredHeight)
                    }
                }
            }
            mManager.rowHeightSizeList.add(maxSize)
        }

        // 处理跨列宽度
        // 自适应规则:如果跨行列单元格宽高大于所跨行列的宽高,那么多出部分均分到所跨行列
        var spanColumnWidth: Int = 0
        var spanRowHeight: Int = 0
        mHelper.spanCellInfo.forEach {
            spanColumnWidth = 0
            spanRowHeight = 0
            for (c in it.cellColumnIndex until it.cellColumnIndex + it.columnSpanSize) {
                spanColumnWidth += mManager.columnWidthSizeList[c]
            }
            for (r in it.cellRowIndex until it.cellRowIndex + it.rowSpanSize) {
                spanRowHeight += mManager.rowHeightSizeList[r]
            }
            val view = getCellView(it.cellRowIndex, it.cellColumnIndex)
            view?.let { v ->
                if (v.measuredWidth > spanColumnWidth) {
                    for (c in it.cellColumnIndex until it.cellColumnIndex + it.columnSpanSize) {
                        mManager.columnWidthSizeList[c] += (v.measuredWidth - spanColumnWidth) / it.columnSpanSize
                    }
                }
                if (v.measuredHeight > spanRowHeight) {
                    for (r in it.cellRowIndex until it.cellRowIndex + it.rowSpanSize) {
                        mManager.rowHeightSizeList[r] += (v.measuredHeight - spanRowHeight) / it.rowSpanSize
                    }
                }
            }
        }

        // 根据父布局宽度,计算应该均分还是横向铺长
        val widthSize = MeasureSpec.getSize(widthMeasureSpec)
        val widthMode = MeasureSpec.getMode(widthMeasureSpec)
        val heightSize = MeasureSpec.getSize(heightMeasureSpec)
        val heightMode = MeasureSpec.getMode(heightMeasureSpec)

        var childrenHeight: Int = 0
        var childrenWidth: Int = 0
        mManager.rowHeightSizeList.forEach { childrenHeight += it }
        mManager.columnWidthSizeList.forEach { childrenWidth += it }

        var widthDimension = 0
        var heightDimension = 0

        when (widthMode) {
            MeasureSpec.EXACTLY -> {
                widthDimension = widthSize
                horizontalScrollEnable = widthSize < childrenWidth
                if (childrenWidth < widthDimension) {
                    // 不足一屏均分剩余宽度
                    val sprideWidth = (widthDimension - childrenWidth) / mManager.columnCount
                    for (i in mManager.columnWidthSizeList.indices) {
                        mManager.columnWidthSizeList[i] =
                            mManager.columnWidthSizeList[i] + sprideWidth
                    }
                }
            }
            MeasureSpec.UNSPECIFIED -> {
                widthDimension = childrenWidth
                horizontalScrollEnable = false
            }
            MeasureSpec.AT_MOST -> {
                widthDimension = widthSize.coerceAtMost(childrenWidth)
                horizontalScrollEnable = widthSize < childrenWidth
            }
        }
        when (heightMode) {
            MeasureSpec.EXACTLY -> {
                heightDimension = heightSize
                verticalScrollEnable = heightSize < childrenHeight
            }
            MeasureSpec.UNSPECIFIED -> {
                heightDimension = childrenHeight
                verticalScrollEnable = false
            }
            MeasureSpec.AT_MOST -> {
                heightDimension = heightSize.coerceAtMost(childrenHeight)
                verticalScrollEnable = heightSize < childrenHeight
            }
        }

        setMeasuredDimension(widthDimension, heightDimension)


        // 第二次 measure 给 child 具体宽高
        children.forEach {
            if (it.layoutParams is LayoutParams) {
                (it.layoutParams as LayoutParams).let { lp ->
                    val spanInfo = mHelper.getSpanInfo(row = lp.row, column = lp.column)
                    spanInfo?.let { span ->
                        // spanInfo 不为空,需要通过跨过的行、列计算宽度
                        var width: Int = 0
                        for (c in span.cellColumnIndex until span.cellColumnIndex + span.columnSpanSize) {
                            width += mManager.columnWidthSizeList[c]
                        }
                        var height: Int = 0
                        for (r in span.cellRowIndex until span.cellRowIndex + span.rowSpanSize) {
                            height += mManager.rowHeightSizeList[r]
                        }

                        lp.width = width
                        lp.height = height

                        it.measure(
                            MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                            MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
                        )
                    } ?: kotlin.run {
                        // spanInfo 为空,证明非跨行/列的单元格
                        lp.width = mManager.columnWidthSizeList[lp.column]
                        lp.height = mManager.rowHeightSizeList[lp.row]

                        it.measure(
                            MeasureSpec.makeMeasureSpec(
                                mManager.columnWidthSizeList[lp.column],
                                MeasureSpec.EXACTLY
                            ),
                            MeasureSpec.makeMeasureSpec(
                                mManager.rowHeightSizeList[lp.row],
                                MeasureSpec.EXACTLY
                            )
                        )
                    }
                }
            }
        }
    }

    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        // layout common first
        children.forEach { it ->
            val lp: LayoutParams? =
                if (it.layoutParams is LayoutParams) it.layoutParams as LayoutParams else null
            lp?.let { lp ->
                var childLeft = (mManager.getLeftSpace(lp.column) + horizontalScrollSpace)
                if (mHelper.stableColumnIndex.contains(lp.column) || mHelper.stickyColumnIndex.contains(
                        lp.column
                    )
                ) {
                    childLeft = childLeft.coerceAtLeast(getLeftStableSpace(lp.column))
                }
                if (mHelper.stickyColumnIndex.contains(lp.column)) {
                    childLeft = childLeft.coerceAtMost(
                        mManager.getLeftSpace(mHelper.getRightSpecialIndex(lp.column)) + horizontalScrollSpace - mManager.columnWidthSizeList[lp.column]
                    )
                }
                var childTop = (mManager.getTopSpace(lp.row) + verticalScrollSpace)
                if (mHelper.stableRowIndex.contains(lp.row) || mHelper.stickyRowIndex.contains(lp.row)) {
                    childTop = childTop.coerceAtLeast(getTopStableSpace(lp.row))
                }
                if (mHelper.stickyRowIndex.contains(lp.row)) {
                    childTop = childTop.coerceAtMost(
                        mManager.getTopSpace(mHelper.getBottomSpecialIndex(lp.row)) + verticalScrollSpace - mManager.rowHeightSizeList[lp.row]
                    )
                }
                it.layout(childLeft, childTop, childLeft + lp.width, childTop + lp.height)
            }
        }
    }

    private fun getLeftStableSpace(column: Int): Int {
        var space = 0
        mHelper.stableColumnIndex.forEach {
            if (it < column) {
                if (mManager.columnWidthSizeList.size > it){
                    space += mManager.columnWidthSizeList[it]
                }
            }
        }
        return space
    }

    private fun getLeftSpicalSpace(column: Int): Int {
        var space = 0
        var leftStableIndex = 0
        mHelper.stableColumnIndex.forEach {
            if (it < column) {
                if (mManager.columnWidthSizeList.size > it) {
                    space += mManager.columnWidthSizeList[it]
                }
                leftStableIndex = leftStableIndex.coerceAtLeast(it)
            }
        }
        mHelper.stickyColumnIndex.forEach {
            if (it in leftStableIndex until column) {
                if (mManager.columnWidthSizeList.size > it) {
                    space += mManager.columnWidthSizeList[it]
                }
            }
        }
        return space
    }

    private fun getTopStableSpace(row: Int): Int {
        var space = 0
        mHelper.stableRowIndex.forEach {
            if (it < row) {
                if (mManager.rowHeightSizeList.size > it) {
                    space += mManager.rowHeightSizeList[it]
                }
            }
        }
        return space
    }

    private fun getTopSpicalSpace(row: Int): Int {
        var space = 0
        var topStableIndex = 0
        mHelper.stableRowIndex.forEach {
            if (it < row) {
                if (mManager.rowHeightSizeList.size > it) {
                    space += mManager.rowHeightSizeList[it]
                }
                topStableIndex = topStableIndex.coerceAtLeast(it)
            }
        }
        mHelper.stickyRowIndex.forEach {
            if (it in topStableIndex until row) {
                if (mManager.rowHeightSizeList.size > it) {
                    space += mManager.rowHeightSizeList[it]
                }
            }
        }
        return space
    }

    override fun dispatchDraw(canvas: Canvas?) {
        super.dispatchDraw(canvas)
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
    }

    private var pressX: Float = 0F
    private var pressY: Float = 0F
    private var lastX: Float = 0F
    private var lastY: Float = 0F

    private var scrollDirecton: Int = SCROLL_DIRECTION_VERTICAL

    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        val result = super.onInterceptTouchEvent(ev)

        if (ev?.action == MotionEvent.ACTION_MOVE && mIsBeingDragged) {
            return true
        }
        when (ev?.action) {
            MotionEvent.ACTION_MOVE -> {
                if (!mIsBeingDragged) {
                    val dx = ev.x - pressX
                    val dy = ev.y - pressY
                    if (dx.absoluteValue > mTouchSlop) {
                        mIsBeingDragged = true

                        lastX = ev.x
                        lastY = ev.y
                    }

                    if (dy.absoluteValue > mTouchSlop) {
                        mIsBeingDragged = true

                        lastX = ev.x
                        lastY = ev.y
                    }

                    scrollDirecton =
                        if (dx.absoluteValue > dy.absoluteValue && horizontalScrollEnable) {
                            SCROLL_DIRECTION_HORIZONTAL
                        } else {
                            SCROLL_DIRECTION_VERTICAL
                        }
                    val parent = parent
                    parent?.requestDisallowInterceptTouchEvent(true)
                }
            }
            MotionEvent.ACTION_DOWN -> {
                startNestedScroll(SCROLL_AXIS_VERTICAL)
                startNestedScroll(SCROLL_AXIS_HORIZONTAL)

                pressX = ev.x
                pressY = ev.y
                lastX = ev.x
                lastY = ev.y

                mIsBeingDragged = false
            }
            MotionEvent.ACTION_CANCEL -> {
                stopNestedScroll()
                mIsBeingDragged = false
            }
            MotionEvent.ACTION_UP -> {
                stopNestedScroll()
                mIsBeingDragged = false
            }
        }
        return result || mIsBeingDragged
    }


    override fun onTouchEvent(event: MotionEvent?): Boolean {
        val result = super.onTouchEvent(event)
        event?.let {
            when (it.action) {
                MotionEvent.ACTION_DOWN -> {
                }
                MotionEvent.ACTION_MOVE -> {
                    if (mIsBeingDragged) {
                        var dx = it.x - lastX
                        var dy = it.y - lastY
                        var unConsumeX = 0F
                        var unConsumeY = 0F

                        lastX = it.x
                        lastY = it.y

                        dispatchNestedPreScroll(-dx.toInt(), -dy.toInt(), mScrollConsumed, null)

                        dx += mScrollConsumed[0]
                        dy += mScrollConsumed[1]

                        lastX += mScrollConsumed[0]
                        lastY += mScrollConsumed[1]

                        if (scrollDirecton == SCROLL_DIRECTION_VERTICAL) {
                            if (verticalScrollSpace + dy > 0) {
                                unConsumeY = dy - verticalScrollSpace
                                verticalScrollSpace = 0
                            } else if (verticalScrollSpace + dy < measuredHeight - mManager.getChildrenHeight()) {
                                unConsumeY =
                                    dy - (measuredHeight - mManager.getChildrenHeight() - verticalScrollSpace)
                                verticalScrollSpace = measuredHeight - mManager.getChildrenHeight()
                            } else {
                                verticalScrollSpace += dy.toInt()
                            }
                            unConsumeX = 0F
                        } else {
                            if (horizontalScrollSpace + dx > 0) {
                                unConsumeX = dx - horizontalScrollSpace
                                horizontalScrollSpace = 0
                            } else if (horizontalScrollSpace + dx < measuredWidth - mManager.getChildrenWidth()) {
                                unConsumeX =
                                    dx - (measuredWidth - mManager.getChildrenWidth() - horizontalScrollSpace)
                                horizontalScrollSpace = measuredWidth - mManager.getChildrenWidth()
                            } else {
                                horizontalScrollSpace += dx.toInt()
                            }
                            unConsumeY = 0F
                        }

                        requestLayout()

                        dispatchNestedScroll(
                            0,
                            0,
                            -unConsumeX.toInt(),
                            -unConsumeY.toInt(),
                            mWindowOffSet
                        )

                        lastX -= mWindowOffSet[0]
                        lastY -= mWindowOffSet[1]
                    }
                }
                else -> {}
            }
        }
        return true
    }

    private fun getCellView(row: Int, column: Int): View? {
        children.forEach {
            if (it.layoutParams is LayoutParams) {
                (it.layoutParams as LayoutParams).let { lp ->
                    if (lp.row == row && lp.column == column) {
                        return it
                    }
                }
            }
        }
        return null
    }

    /**
     * 单元格Adapter
     *
     * cell 单元格
     */
    abstract class Adapter() {

        var updateListener: ((column: Int, row: Int) -> Unit)? = null

        /**
         * 创建view
         */
        abstract fun createCellView(parent: View, column: Int, row: Int, cellType: Int): View

        /**
         * 绑定数据
         *
         * @param cellView 单元格view
         * @param column 列
         * @param row 行
         * @param cellType 单元格类型
         */
        abstract fun bindData(cellView: View, column: Int, row: Int, cellType: Int)

        /**
         * 获取列数
         */
        abstract fun getColumnCount(): Int

        /**
         * 获取行数
         */
        abstract fun getRowCount(): Int

        /**
         * 单元类型
         */
        open fun getCellType(column: Int, row: Int): Int = 1

        fun notifyDataSetChanged() {
            updateListener?.invoke(-1, -1)
        }

        fun notifyItemChanged(column: Int, row: Int) {
            updateListener?.invoke(column, row)
        }
    }

    class LayoutParams : ViewGroup.LayoutParams(0, 0) {

        var row: Int = 0
        var column: Int = 0

    }

    /**
     * 单元格管理类
     */
    private class CellsManager {

        var rowCount: Int = 0

        var columnCount: Int = 0

        /**
         * 每行行高
         */
        val rowHeightSizeList: MutableList<Int> = mutableListOf()

        /**
         * 每列列宽
         */
        val columnWidthSizeList: MutableList<Int> = mutableListOf()

        /**
         * 重置
         */
        fun reset() {
            rowCount = 0
            columnCount = 0
            rowHeightSizeList.clear()
            columnWidthSizeList.clear()
        }

        fun getLeftSpace(column: Int): Int {
            var space = 0
            for (i in 0 until column.coerceAtMost(columnCount)) {
                if (columnWidthSizeList.size > i) {
                    space += columnWidthSizeList[i]
                }
            }
            return space
        }

        fun getTopSpace(row: Int): Int {
            var space = 0
            for (i in 0 until row.coerceAtMost(rowCount)) {
                if (rowHeightSizeList.size > i) {
                    space += rowHeightSizeList[i]
                }
            }
            return space
        }

        fun getChildrenWidth(): Int {
            var space = 0
            columnWidthSizeList.forEach {
                space += it
            }
            return space
        }

        fun getChildrenHeight(): Int {
            var space = 0
            rowHeightSizeList.forEach {
                space += it
            }
            return space
        }
    }

    /**
     * 表格布局管理类
     *
     * 记录 跨行列、固定展示、粘性吸附属性信息
     */
    private class LayoutHelper {

        /**
         * 跨行列单元格信息
         */
        val spanCellInfo: MutableList<SpanCellItemInfo> = mutableListOf()

        /**
         * 固定展示行信息
         */
        val stableRowIndex: MutableList<Int> = mutableListOf()

        /**
         * 固定展示列信息
         */
        val stableColumnIndex: MutableList<Int> = mutableListOf()

        /**
         * 粘性吸附行信息
         */
        val stickyRowIndex: MutableList<Int> = mutableListOf()

        /**
         * 粘性吸附列信息
         */
        val stickyColumnIndex: MutableList<Int> = mutableListOf()

        /**
         * 判断单元格是否展示
         */
        fun showCell(column: Int, row: Int): Boolean {
            spanCellInfo.forEach {
                if (column == it.cellColumnIndex && row == it.cellRowIndex) {
                    return true
                }
                if (it.cellColumnIndex <= column
                    && it.cellColumnIndex + it.columnSpanSize > column
                    && it.cellRowIndex <= row
                    && it.cellRowIndex + it.rowSpanSize > row
                ) {
                    return false
                }
            }
            return true
        }

        fun isSpanCell(column: Int, row: Int): Boolean {
            spanCellInfo.forEach {
                if (it.cellColumnIndex == column && it.cellRowIndex == row) {
                    return true
                }
            }
            return false
        }

        /**
         * 获取跨行列信息
         *
         * @return 如果单元格不存在跨行列信息,返回null
         */
        fun getSpanInfo(column: Int, row: Int): SpanCellItemInfo? {
            spanCellInfo.forEach {
                if (it.cellRowIndex == row && it.cellColumnIndex == column) {
                    return it
                }
            }
            return null
        }

        fun isStableCell(column: Int, row: Int): Boolean {
            stableColumnIndex.forEach {
                if (column == it) {
                    return true
                }
            }
            stableRowIndex.forEach {
                if (row == it) {
                    return true
                }
            }
            return false
        }

        fun isStickyCell(column: Int, row: Int): Boolean {
            stickyColumnIndex.forEach {
                if (column == it) {
                    return true
                }
            }
            stickyRowIndex.forEach {
                if (row == it) {
                    return true
                }
            }
            return false
        }

        fun getRightSpecialIndex(column: Int): Int {
            var index = Int.MAX_VALUE
            stableColumnIndex.forEach {
                if (it > column) {
                    index = index.coerceAtMost(it)
                }
            }
            stickyColumnIndex.forEach {
                if (it > column) {
                    index = index.coerceAtMost(it)
                }
            }
            return index
        }

        fun getBottomSpecialIndex(row: Int): Int {

            var index = Int.MAX_VALUE
            stableRowIndex.forEach {
                if (it > row) {
                    index = index.coerceAtMost(it)
                }
            }
            stickyRowIndex.forEach {
                if (it > row) {
                    index = index.coerceAtMost(it)
                }
            }
            return index
        }
    }
}

data class SpanCellItemInfo(
    val cellRowIndex: Int = 0,
    val cellColumnIndex: Int = 0,
    val rowSpanSize: Int = 1,
    val columnSpanSize: Int = 1
)
package com.elee.uidemo

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity

class TableDemoActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_table_demo)

        findViewById<ZrTableView>(R.id.view_table).apply {

            // 合并单元格
//            setSpanList(mutableListOf(SpanCellItemInfo(3, 3, 2, 2)))
            
            // 吸附行列
//            setStableColumnList(mutableListOf(1,4))
//            setStableRowList(mutableListOf(1,4))
//            setStickyColumnList(mutableListOf(2))
//            setStickyRowList(mutableListOf(2))

            adapter = object : ZrTableView.Adapter() {
                override fun createCellView(
                    parent: View,
                    column: Int,
                    row: Int,
                    cellType: Int
                ): View = LayoutInflater.from(parent.context)
                    .inflate(R.layout.layout_table_cell_item, parent as ViewGroup, false)

                override fun bindData(cellView: View, column: Int, row: Int, cellType: Int) {
                    cellView.findViewById<TextView>(R.id.tv_item).text =
                        "row: $row\ncolumn: $column"
                    cellView.findViewById<TextView>(R.id.tv_item).setOnClickListener {  }
                }

                override fun getColumnCount(): Int = 10

                override fun getRowCount(): Int = 20
            }
        }
    }
}
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.elee.uidemo.ZrTableView
        android:id="@+id/view_table"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</FrameLayout>
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/tv_item"
        android:layout_width="150dp"
        android:layout_height="80dp"
        android:background="@color/white"
        android:gravity="center"
        android:text="row:1\ncolumn: 1"
        android:textSize="15sp" />

    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="#EEE" />

    <View
        android:layout_width="1dp"
        android:layout_height="match_parent"
        android:background="#EEE" />
</FrameLayout>
  • 7
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值