简单实现一个关系图View

前言

 

前两天,leader拿了一张图过来,竟然问我能不能在app里做出来...这不是废话么,时间给够就没有画不出来的...

分析一下,这就是一张关系图,目前看是三个列表中间有连接线进行关联,每列数据样式都不一样。

OK,这么简单的View,还是要设计一下

设计

首先,这个关系图顶点数量不固定,列数多半也不是固定的,所以定义适配器 Adapter 去把数据转换成 View

其次,将 View 正确排列到关系图中,最简单暴力的方式就是在 addView 的时候设置 LayoutParams 等参数;其次就是在 onMeasure & onLayout 方法中进行布局,这样即使之后要增加选中动画等复杂操作也比较容易;最优美的方式就是学习 RecyclerView 一样,通过 LayoutManager 进行处理,这样符合了单一原则,之后拓展样式时也不需要修改老代码。

最后,对于连接线的绘制,能看出线是链接两个顶点 View 左右两边的中点,并且都是一个颜色没有区别的。所以简单暴力的方法就是在 ViewGroup 的 draw 方法中直接根据左右两列中需要链接的 View 的坐标绘制一条线;复杂的方法就是定义 BaseLine 将线条的绘制进行封装,方便以后拓展。

OK,夏姬八想了够久了,先按照最简单的方式写个 Demo 给组长交差吧

Demo 效果图

 

代码

view

package com.xxx

import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import com.xxx.R

/**
 * 多层级关系图
 */
class FloorRelationMapView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
    ConstraintLayout(context, attrs, defStyleAttr) {

    /**
     * 水平方向间距
     */
    var horizontalSpace: Int = 0

    /**
     * 垂直方向最小间距
     */
    var verticalSpace: Int = 0

    var lineWidth: Float = 0F

    var lineColor: Int = Color.GRAY

    var adapter: BaseFloorRelationAdapter? = null
        set(value) {
            field = value
            value?.dataSetChangedListener =
                object : BaseFloorRelationAdapter.IOnDataSetChangedListener {
                    override fun dataSetChanged(floor: Int, position: Int) {
                        updateViews(floor, position)
                    }
                }
            updateViews()
        }
    
    /**
     * 按照列-行管理View的二维数组
     * 在 onDraw 的时候遍历用
     * 
     * 应该有别的方案,但是为了尽快实现 Demo 先不管了
     */
    private val pointViews: ArrayList<ArrayList<View>> = ArrayList()

    constructor(context: Context) : this(context, null, 0)

    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)

    init {
        setWillNotDraw(false)

        // 获取自定义参数
        val array = getContext().obtainStyledAttributes(attrs, R.styleable.FloorRelationMapView)
        if (array.hasValue(R.styleable.FloorRelationMapView_verticalSpace)) {
            verticalSpace =
                array.getDimension(R.styleable.FloorRelationMapView_verticalSpace, 0F).toInt()
        }
        if (array.hasValue(R.styleable.FloorRelationMapView_horizontalSpace)) {
            horizontalSpace =
                array.getDimension(R.styleable.FloorRelationMapView_horizontalSpace, 0F).toInt()
        }
        if (array.hasValue(R.styleable.FloorRelationMapView_lineWidth)) {
            lineWidth = array.getDimension(R.styleable.FloorRelationMapView_lineWidth, 0F)
        }
        if (array.hasValue(R.styleable.FloorRelationMapView_lineColor)) {
            lineColor = array.getColor(R.styleable.FloorRelationMapView_lineColor, Color.GRAY)
        }
    }

    /**
     * 更新视图view
     *
     * @param floor 数据改变层级 -1表示全量更新
     * @param position 数据改变位置 -1表示全量更新
     */
    private fun updateViews(floor: Int = -1, position: Int = -1) {
        if (adapter == null) {
            return
        }

        if (floor != -1 && floor < adapter?.getFloorsCount() ?: 0 && floor < pointViews.size) {
            val floorViews: ArrayList<View> = pointViews[floor]
            if (position != -1 && position < adapter?.getPointCount(floor) ?: 0 && position < floorViews.size) {
                // 仅替换一个
                var pointView: View? = floorViews[position]
                val layoutParam: ViewGroup.LayoutParams =
                    pointView?.layoutParams ?: LayoutParams(
                        0,
                        LayoutParams.WRAP_CONTENT
                    )
                removeView(pointView)
                pointView = adapter!!.getView(floor, position, this)
                pointView.layoutParams = layoutParam
                floorViews[position] = pointView
                addView(pointView)
            } else {
                // 替换整列view
                for (i in 0 until (adapter?.getPointCount(floor) ?: 0)) {
                    var pointView: View = floorViews[i]
                    removeView(pointView)

                    pointView = adapter!!.getView(floor, i, this)
                    pointView.id = generateViewId()
                    val layoutParam: LayoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT)
                    if (i == 0) {
                        // 每列第一个 view
                        layoutParam.verticalChainStyle = LayoutParams.CHAIN_SPREAD
                        layoutParam.topToTop = id

                        // 与左侧列关联
                        val leftView: View? =
                            if (floor > 0) null else pointViews[floor - 1][0]
                        if (leftView == null) {
                            layoutParam.leftToLeft = id
                            layoutParam.horizontalChainStyle = LayoutParams.CHAIN_SPREAD_INSIDE
                        } else {
                            layoutParam.leftToRight = leftView.id
                            (leftView.layoutParams as LayoutParams).rightToLeft = pointView.id
                        }

                        // 与右侧列关联
                        val rightView: View? =
                            if (floor < adapter!!.getFloorsCount() - 1) pointViews[floor - 1][0] else null
                        if (rightView == null) {
                            layoutParam.rightToRight = id
                        } else {
                            layoutParam.marginEnd = horizontalSpace

                            layoutParam.rightToLeft = rightView.id
                            (rightView.layoutParams as LayoutParams).leftToRight = pointView.id
                        }
                    } else {
                        layoutParam.topMargin = verticalSpace

                        val topView: View = pointViews[floor][i - 1]

                        // 上下成链
                        (topView.layoutParams as LayoutParams).bottomToTop = pointView.id
                        layoutParam.topToBottom = topView.id

                        // 左右对齐
                        layoutParam.leftToLeft = topView.id
                        layoutParam.rightToRight = topView.id
                    }

                    // 每列最后一个 view
                    if (i == adapter!!.getPointCount(floor) - 1) {
                        layoutParam.bottomToBottom = id
                    }

                    pointView.layoutParams = layoutParam
                    floorViews[position] = pointView
                    addView(pointView)
                }
            }
        } else {
            // 替换所有view
            removeAllViews()
            pointViews.clear()
            for (i in 0 until (adapter?.getFloorsCount() ?: 0)) {

                if (adapter!!.getPointCount(i) == 0) {
                    continue
                }

                val floorViews: ArrayList<View> = ArrayList()
                for (j in 0 until (adapter?.getPointCount(i) ?: 0)) {
                    var pointView: View = adapter!!.getView(i, j, this)
                    pointView.id = generateViewId()

                    val layoutParam: LayoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT)
                    if (j == 0) {
                        // 每列第一个 view
                        layoutParam.verticalChainStyle = LayoutParams.CHAIN_SPREAD
                        layoutParam.topToTop = id

                        // 需要与左侧 view 关联
                        val leftView: View? =
                            if (i > 0) pointViews[i - 1][0] else null
                        if (leftView == null) {
                            layoutParam.leftToLeft = id
                            layoutParam.horizontalChainStyle = LayoutParams.CHAIN_SPREAD_INSIDE
                        } else {
                            layoutParam.leftToRight = leftView.id
                            (leftView.layoutParams as LayoutParams).rightToLeft = pointView.id
                        }

                        // 最后一列的第一个 view
                        if (i == adapter!!.getFloorsCount() - 1) {
                            layoutParam.rightToRight = id
                        } else {
                            layoutParam.marginEnd = horizontalSpace
                        }
                    } else {
                        layoutParam.topMargin = verticalSpace

                        val topView: View = floorViews[j - 1]

                        // 上下成链
                        (topView.layoutParams as LayoutParams).bottomToTop = pointView.id
                        layoutParam.topToBottom = topView.id

                        // 左右对齐
                        layoutParam.leftToLeft = topView.id
                        layoutParam.rightToRight = topView.id
                    }

                    // 每列最后一个 view
                    if (j == adapter!!.getPointCount(i) - 1) {
                        layoutParam.bottomToBottom = id
                    }

                    pointView.layoutParams = layoutParam
                    floorViews.add(pointView)
                    addView(pointView)
                }
                pointViews.add(floorViews)
            }
        }

        invalidate()
    }

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

        val paint: Paint = Paint()
        paint.color = lineColor
        paint.strokeWidth = lineWidth

        // 遍历左右两列数据的View,判断是否需要绘制连接线
        // 最好改成从 Adapter 中获取关联关系,然后根据关系再找对应的 View 信息,然后绘制
        for (i in 1 until pointViews.size) {
            for (l in pointViews[i - 1].indices) {
                for (r in pointViews[i].indices) {
                    if (adapter?.isRelationCollect(i - 1, l, i, r) == true) {
                        canvas?.drawLine(
                            (pointViews[i - 1][l].left + pointViews[i - 1][l].measuredWidth).toFloat(),
                            (pointViews[i - 1][l].top + pointViews[i - 1][l].measuredHeight / 2).toFloat(),
                            pointViews[i][r].left.toFloat(),
                            (pointViews[i][r].top + pointViews[i][r].measuredHeight / 2).toFloat(),
                            paint
                        )
                    }
                }
            }
        }
    }
}

adapter


package com.xxx

import android.view.View
import android.view.ViewGroup

abstract class BaseFloorRelationAdapter {

    var dataSetChangedListener: IOnDataSetChangedListener? = null

    /**
     * 获取层级数量
     */
    abstract fun getFloorsCount(): Int

    /**
     * 获取对应层级下顶点数量
     */
    abstract fun getPointCount(floor: Int): Int

    /**
     * 获取顶点视图
     */
    abstract fun getView(floor: Int, position: Int, parent: ViewGroup): View

    /**
     * 是否有关系
     */
    abstract fun isRelationCollect(
        leftFloor: Int,
        leftPosition: Int,
        rightFloor: Int,
        rightPosition: Int
    ): Boolean

    /**
     * 通知数据更新
     *
     * @param floor 数据改变层级 -1表示全量更新
     * @param position 数据改变位置 -1表示全量更新
     */
    fun notifyDataSetChanged(floor: Int = -1, position: Int = -1) {
        dataSetChangedListener?.dataSetChanged(floor, position)
    }

    /**
     * 数据改变监听
     */
    interface IOnDataSetChangedListener {
        /**
         * @param floor 数据改变层级 -1表示全量更新
         * @param position 数据改变位置 -1表示全量更新
         */
        fun dataSetChanged(floor: Int = -1, position: Int = -1)
    }
}

demoActivity


package com.xxx

import android.app.Activity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import com.xxx.R
/**
 * Demo页
 */
class FloorRelationMapDemoActivity : Activity() {

    val mapView: FloorRelationMapView by lazy { findViewById<FloorRelationMapView>(R.id.map) }

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

        val data = listOf(
            listOf<String>("a1", "a2", "a3"),
            listOf<String>("b1", "b2", "b3", "b4", "b5"),
            listOf<String>("c1", "c2", "c3")
        )

        val adapter: BaseFloorRelationAdapter = object : BaseFloorRelationAdapter() {
            override fun getFloorsCount(): Int = data.size

            override fun getPointCount(floor: Int): Int = data[floor].size

            override fun getView(floor: Int, position: Int, parent: ViewGroup): View {
                val view: View = LayoutInflater.from(this@FloorRelationMapDemoActivity)
                    .inflate(R.layout.b_item_floor_relation_demo, parent, false)
                view.findViewById<TextView>(R.id.tv_text).text = data[floor][position]
                return view
            }

            /**
             * 随便实现的,因为没想好链接关系的数据结构
             */
            override fun isRelationCollect(
                leftFloor: Int,
                leftPosition: Int,
                rightFloor: Int,
                rightPosition: Int
            ): Boolean = (leftFloor + leftPosition + rightFloor + rightPosition) % 3 == 0
        }
        mapView.adapter = adapter
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值