You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Android中如何用圆形和线条Drawable实现带动态节点的三角形布局?

嘿,我正好有几个适合你的实现思路,完全基于你现有的圆形和线条Drawable,而且能轻松满足节点动态显示数据的需求,一起来看看:

方案一:自定义View直接绘制(最灵活)

这个方案直接在Canvas上搞定所有元素,能完美适配各种节点位置和动态数据变化,步骤也很清晰:

  1. 继承View,在onSizeChanged里根据View的宽高计算三角形三个顶点的坐标(比如正三角形可以以View中心为基准来算)
  2. 重写onDraw方法:
    • Paint绘制三条边(或者直接用你的线条Drawable,通过setBounds定位到对应边的位置)
    • 提前计算好所有节点的坐标(包括顶点和边上的节点)
    • 遍历每个节点,先绘制圆形Drawable(以节点坐标为中心设置bounds),再用drawText绘制动态文本
  3. 提供一个更新节点数据的方法,调用invalidate刷新视图就行

给你一段Kotlin示例代码参考:

class TriangleNodeView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    // 引入你已有的圆形和线条资源(这里用示例ID,替换成你的实际资源)
    private val circleDrawable = ContextCompat.getDrawable(context, R.drawable.circle)!!
    private val linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.BLACK
        strokeWidth = 4f
        style = Paint.Style.STROKE
    }
    // 用于绘制节点文本的画笔
    private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.WHITE
        textSize = 24f
        textAlign = Paint.Align.CENTER
    }

    // 存储三角形三个顶点坐标
    private var trianglePoints = listOf<PointF>()
    // 存储节点数据:坐标+文本
    private var nodes = listOf<NodeData>()

    data class NodeData(val x: Float, val y: Float, val text: String)

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        // 计算正三角形的三个顶点(可根据需求调整形状)
        val centerX = w / 2f
        val centerY = h / 2f
        val sideLength = min(w, h) * 0.8f // 取宽高中较小值的80%作为边长
        trianglePoints = listOf(
            PointF(centerX, centerY - sideLength / kotlin.math.sqrt(3f)), // 顶部顶点
            PointF(centerX - sideLength / 2f, centerY + sideLength / (2 * kotlin.math.sqrt(3f))), // 左下角
            PointF(centerX + sideLength / 2f, centerY + sideLength / (2 * kotlin.math.sqrt(3f)))  // 右下角
        )
        // 生成示例节点(包括顶点和每条边的中点)
        nodes = listOf(
            NodeData(trianglePoints[0].x, trianglePoints[0].y, "A"),
            NodeData((trianglePoints[0].x + trianglePoints[1].x)/2, (trianglePoints[0].y + trianglePoints[1].y)/2, "B"),
            NodeData(trianglePoints[1].x, trianglePoints[1].y, "C"),
            NodeData((trianglePoints[1].x + trianglePoints[2].x)/2, (trianglePoints[1].y + trianglePoints[2].y)/2, "D"),
            NodeData(trianglePoints[2].x, trianglePoints[2].y, "E"),
            NodeData((trianglePoints[2].x + trianglePoints[0].x)/2, (trianglePoints[2].y + trianglePoints[0].y)/2, "F")
        )
    }

    // 外部调用更新节点数据
    fun updateNodeData(newNodes: List<NodeData>) {
        nodes = newNodes
        invalidate() // 触发重绘
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        // 绘制三角形三条边
        canvas.drawLine(trianglePoints[0].x, trianglePoints[0].y, trianglePoints[1].x, trianglePoints[1].y, linePaint)
        canvas.drawLine(trianglePoints[1].x, trianglePoints[1].y, trianglePoints[2].x, trianglePoints[2].y, linePaint)
        canvas.drawLine(trianglePoints[2].x, trianglePoints[2].y, trianglePoints[0].x, trianglePoints[0].y, linePaint)

        // 绘制每个节点:先画圆形,再画文本
        nodes.forEach { node ->
            val circleSize = circleDrawable.intrinsicWidth
            // 设置圆形Drawable的位置,以节点坐标为中心
            circleDrawable.setBounds(
                (node.x - circleSize/2).toInt(),
                (node.y - circleSize/2).toInt(),
                (node.x + circleSize/2).toInt(),
                (node.y + circleSize/2).toInt()
            )
            circleDrawable.draw(canvas)
            // 文本垂直居中绘制
            val textY = node.y - (textPaint.descent() + textPaint.ascent()) / 2
            canvas.drawText(node.text, node.x, textY, textPaint)
        }
    }
}
方案二:用ConstraintLayout组合视图(更易维护)

如果不想写自定义View,用ConstraintLayout搭布局是个更省心的选择,完全通过布局约束来定位元素:

  1. 用三个View作为三角形的三条边,通过ConstraintLayout的约束把它们拼成三角形(比如顶部边连接左下角和右下角节点,左边连接顶部和左下角节点)
  2. 在每个节点位置放ImageView(显示你的圆形Drawable)和TextView(显示动态数据),用约束把它们定位到对应位置(比如边上的中间节点,可以用约束让它和边的两端等距)
  3. 动态更新数据只需要修改对应TextView的文本就行,不用处理绘制逻辑

给你一段布局文件示例:

<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- 三角形三条边 -->
    <View
        android:id="@+id/line_top_left"
        android:layout_width="0dp"
        android:layout_height="4dp"
        android:background="@drawable/line"
        app:layout_constraintStart_toStartOf="@id/node_bottom_left"
        app:layout_constraintEnd_toEndOf="@id/node_top"
        app:layout_constraintTop_toTopOf="@id/node_top"
        app:layout_constraintBottom_toBottomOf="@id/node_bottom_left"/>

    <View
        android:id="@+id/line_bottom"
        android:layout_width="0dp"
        android:layout_height="4dp"
        android:background="@drawable/line"
        app:layout_constraintStart_toStartOf="@id/node_bottom_left"
        app:layout_constraintEnd_toEndOf="@id/node_bottom_right"
        app:layout_constraintTop_toTopOf="@id/node_bottom_left"
        app:layout_constraintBottom_toBottomOf="@id/node_bottom_right"/>

    <View
        android:id="@+id/line_top_right"
        android:layout_width="0dp"
        android:layout_height="4dp"
        android:background="@drawable/line"
        app:layout_constraintStart_toStartOf="@id/node_top"
        app:layout_constraintEnd_toEndOf="@id/node_bottom_right"
        app:layout_constraintTop_toTopOf="@id/node_top"
        app:layout_constraintBottom_toBottomOf="@id/node_bottom_right"/>

    <!-- 顶部节点 -->
    <ImageView
        android:id="@+id/node_top"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/circle"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

    <TextView
        android:id="@+id/text_top"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="A"
        android:textColor="@android:color/white"
        app:layout_constraintCenter_toCenterOf="@id/node_top"/>

    <!-- 左下角节点 -->
    <ImageView
        android:id="@+id/node_bottom_left"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/circle"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toStartOf="@id/node_bottom_right"/>

    <TextView
        android:id="@+id/text_bottom_left"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="C"
        android:textColor="@android:color/white"
        app:layout_constraintCenter_toCenterOf="@id/node_bottom_left"/>

    <!-- 右下角节点 -->
    <ImageView
        android:id="@+id/node_bottom_right"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/circle"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@id/node_bottom_left"/>

    <TextView
        android:id="@+id/text_bottom_right"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="E"
        android:textColor="@android:color/white"
        app:layout_constraintCenter_toCenterOf="@id/node_bottom_right"/>

    <!-- 左边中间节点 -->
    <ImageView
        android:id="@+id/node_middle_left"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/circle"
        app:layout_constraintStart_toStartOf="@id/line_top_left"
        app:layout_constraintEnd_toEndOf="@id/line_top_left"
        app:layout_constraintTop_toTopOf="@id/line_top_left"
        app:layout_constraintBottom_toBottomOf="@id/line_top_left"/>

    <TextView
        android:id="@+id/text_middle_left"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="B"
        android:textColor="@android:color/white"
        app:layout_constraintCenter_toCenterOf="@id/node_middle_left"/>

    <!-- 右边和底部中间节点可以复制上面的结构,修改约束即可 -->

</androidx.constraintlayout.widget.ConstraintLayout>
方案三:混合模式(精确边框+视图组合)

如果想要精确的三角形边框,又不想放弃视图组合的便捷性,可以在ConstraintLayout里放一个只画三角形边框的自定义View,然后在它上面叠加ImageView和TextView作为节点,这样兼顾两者的优点。

选择建议:

  • 如果节点数量、位置经常变化,或者需要加动画效果,优先选方案一,灵活性拉满
  • 如果节点位置固定,只是数据动态更新,方案二最省事,不用写自定义代码
  • 想要精确边框又想保留视图组合的方便,就选方案三

内容的提问来源于stack exchange,提问作者Bhargav Thanki

火山引擎 最新活动