前言
?
前两天,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
}
}
|