前言
前两天,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()
}
private val pointViews: ArrayList> = 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)
}
}
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 = 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 = 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
fun notifyDataSetChanged(floor: Int = -1, position: Int = -1) {
dataSetChangedListener?.dataSetChanged(floor, position)
}
interface IonDataSetChangedListener {
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
class FloorRelationMapDemoActivity : Activity() {
val mapView: FloorRelationMapView by lazy { findViewById(R.id.map) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.b_activity_floor_relation_map_demo)
val data = listOf(
listOf("a1", "a2", "a3"),
listOf("b1", "b2", "b3", "b4", "b5"),
listOf("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(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
}
}



