欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android自定义view、动手实现一个简单的xmind思维导图结构

程序员文章站 2022-03-13 22:20:51
文章目录1、思路拆分2、细分实现2-1、文字换行实现思路2-2、贝塞尔曲线弧度实现思路3、核心源代码老样子,先放效果图吸引火力1、思路拆分猛一看这个图,感觉有种无从小手的感觉是不是?那么让我先来拆分下思路,相信你会觉得很简单。首先这个思维导图构成是由主题:子节点:其中子节点支持控制都是还有子节点链接线:链接线是支持弧度的,第一感觉就是要用贝塞尔曲线来实现。2、细分实现其实观察下来,实现上面的难点主要是两个,第一个是文字换行,第二个是贝塞尔曲线弧度。下面我来一个个拆解下。2-...


Android自定义view、动手实现一个简单的xmind思维导图结构

老样子,先放效果图吸引火力

1、思路拆分

猛一看这个图,感觉有种无从小手的感觉是不是?

那么让我先来拆分下思路,相信你会觉得很简单。

首先这个思维导图构成是由

主题:
Android自定义view、动手实现一个简单的xmind思维导图结构
子节点:
Android自定义view、动手实现一个简单的xmind思维导图结构
其中子节点支持控制都是还有子节点

链接线:
Android自定义view、动手实现一个简单的xmind思维导图结构
链接线是支持弧度的,第一感觉就是要用贝塞尔曲线来实现。

2、细分实现

其实观察下来,实现上面的难点主要是两个,第一个是文字换行,第二个是贝塞尔曲线弧度。下面我来一个个拆解下。

2-1、文字换行实现思路

如果是熟悉自定义view的同学,肯定知道单纯的canvas.drawText是不能实现文字换行的。你一定会说textview也是一个自定义view啊,为啥人家可以?如果你看过textview的源码,你一定会发现textview的所有核心操作,都是在三个layout中,分别是boringlayoutstaticlayoutdynamiclayout。这三个layout分别是对应了单行、静态、动态的效果。而且这三个layout是支持用户使用的,api对外是开放的。所以如果想要实现换行。直接使用staticlayout.draw(canvas)即可。当然具体参数和操作,还要你自行去看源代码了,builder,你懂得,很方便。

除了使用layout,还有没有其他方式呢?收到layout的启发。我这里的实现,其实是比较取巧的,我直接使用textview来实现,最终在canvas里面执行textview.draw(canvas)。这样的话,很多绘制计算对其都不用算了。

 fun doRender(canvas: Canvas?) {
        measure(0, 0)
        layout(xMindNode.rect.left, xMindNode.rect.top, xMindNode.rect.right, xMindNode.rect.bottom)
        canvas?.save()
        canvas?.translate(xMindNode.rect.left.toFloat(), xMindNode.rect.top.toFloat())
        draw(canvas)
        canvas?.restore()
    }

细心的同学可能发现了,这里在draw之前,还进行了measure和layout,以及画布平移。
这里measure和layout其实就是为了在画之前计算出来布局的大小。平移是因为canvas默认会绘制在0,0位置,所以我们要平移到制定的位置。

2-2、贝塞尔曲线弧度实现思路

Android自定义view、动手实现一个简单的xmind思维导图结构
其实核心就是对两端进行贝塞尔曲线处理,完了链接对theme和childnode之间。

canvas.drawLine(0, getHeight() / 2, getWidth(), getHeight() / 2, new Paint());
        canvas.drawLine(getWidth() / 2, 0, getWidth() / 2, getHeight(), new Paint());

        canvas.drawRect(getWidth() / 2 - 50, getHeight() / 2 - 500,
                getWidth() / 2 + 50, getHeight() / 2 + 500, new Paint());

        Paint linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        linePaint.setStrokeWidth(4);
        linePaint.setStyle(Paint.Style.STROKE);
        linePaint.setColor(Color.parseColor("#F14400"));

        Path path = new Path();
        path.moveTo(getWidth() / 2 - 50, getHeight() / 2 - 500);
        path.cubicTo(getWidth() / 2 - 50, getHeight() / 2 - 500,
                getWidth() / 2, getHeight() / 2 - 450,
                getWidth() / 2, getHeight() / 2 - 300
        );
        path.cubicTo(getWidth() / 2, getHeight() / 2 + 300,
                getWidth() / 2, getHeight() / 2 + 450,
                getWidth() / 2 + 50, getHeight() / 2 + 500
        );
        canvas.drawPath(path, linePaint);

3、核心源代码

package org.fireking.ap.custom.basic.viewgroup

import android.content.Context
import android.graphics.Canvas
import android.graphics.PointF
import android.widget.LinearLayout
import org.jetbrains.anko.dip

abstract class NodeRenderer(context: Context?, private val xMindNode: XMindNode) :
    LinearLayout(context) {

    fun doRender(canvas: Canvas?) {
        measure(0, 0)
        layout(xMindNode.rect.left, xMindNode.rect.top, xMindNode.rect.right, xMindNode.rect.bottom)
        canvas?.save()
        canvas?.translate(xMindNode.rect.left.toFloat(), xMindNode.rect.top.toFloat())
        draw(canvas)
        canvas?.restore()
    }

    fun getLeftHotSpot(): PointF {
        return PointF(
            xMindNode.rect.left.toFloat() + dip(2),
            (xMindNode.rect.top + xMindNode.rect.height() / 2).toFloat()
        )
    }

    fun getRightHotSpot(): PointF {
        return PointF(
            xMindNode.rect.right.toFloat() - dip(2),
            (xMindNode.rect.top + xMindNode.rect.height() / 2).toFloat()
        )
    }

    fun getLeftX(): Float {
        return xMindNode.rect.left.toFloat() + dip(2)
    }

    fun getLeftY(): Float {
        return (xMindNode.rect.top + xMindNode.rect.height() / 2).toFloat()
    }

    fun getRightX(): Float {
        return xMindNode.rect.right.toFloat() - dip(2)
    }

    fun getRightY(): Float {
        return (xMindNode.rect.top + xMindNode.rect.height() / 2).toFloat()
    }
}
package org.fireking.ap.custom.basic.viewgroup

import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import org.fireking.ap.R
import org.jetbrains.anko.dip

class XMindView : View {

    private var themeNode: ThemeNode? = null
    private var childNodeList = ArrayList<ChildNode>()

    private var viewWidth = 0
    private var viewHeight = 0

    private lateinit var themeSize: XMindNodeSize
    private lateinit var childNodeSize: XMindNodeSize

    private val themeRectPaint = Paint(Paint.ANTI_ALIAS_FLAG)
    private val childRectPaint = Paint(Paint.ANTI_ALIAS_FLAG)
    private val lineRectPaint = Paint(Paint.ANTI_ALIAS_FLAG)

    private var nodeTopBottomMargin: Int = 0
    private var nodeLeftRightMargin: Int = 0

    private val linePath = Path()

    constructor(context: Context) : super(context) {
        initView(context, null)
    }

    constructor(context: Context, attributes: AttributeSet) : super(context, attributes) {
        initView(context, attributes)
    }

    private fun initView(context: Context, attributes: AttributeSet?) {
        themeRectPaint.color = Color.parseColor("#1777FF")
        themeRectPaint.style = Paint.Style.FILL

        childRectPaint.color = Color.parseColor("#A9D8FF")
        childRectPaint.style = Paint.Style.FILL

        lineRectPaint.color = Color.parseColor("#66BAFF")
        lineRectPaint.style = Paint.Style.STROKE
        lineRectPaint.strokeWidth = dip(2).toFloat()

        nodeTopBottomMargin = dip(8)
        nodeLeftRightMargin = dip(24)
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        viewWidth = w
        viewHeight = h
        initNodeSize()
    }

    private fun initNodeSize() {
        val themeLayout = LayoutInflater.from(context).inflate(R.layout.theme_node, null)
        themeLayout.measure(0, 0)
        themeSize = XMindNodeSize(themeLayout.measuredWidth, themeLayout.measuredHeight)
        val childLayout = LayoutInflater.from(context).inflate(R.layout.child_node, null)
        childLayout.measure(0, 0)
        childNodeSize = XMindNodeSize(childLayout.measuredWidth, childLayout.measuredHeight)
    }

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

        themeNode?.let { theme ->

            // 绘制子节点
            childNodeList.forEachIndexed { _, xMindNode ->
                xMindNode.doRender(canvas)
                drawLinkLine(canvas, theme, xMindNode)
            }

            // 绘制主题节点
            theme.doRender(canvas)
        }
    }

    private fun drawLinkLine(canvas: Canvas?, theme: ThemeNode, child: ChildNode) {
        linePath.reset()
        if (child.isLeft()) {
            linePath.moveTo(child.getRightX(), child.getRightY())
            val lineMaxLength = theme.getLeftY() - child.getRightY()
            linePath.cubicTo(
                child.getRightX(),
                child.getRightY(),
                child.getRightX() + nodeLeftRightMargin / 2,
                child.getRightY() + lineMaxLength / 10,
                child.getRightX() + nodeLeftRightMargin / 2,
                child.getRightY() + lineMaxLength / 10 * 3
            )
            linePath.cubicTo(
                theme.getLeftX() - nodeLeftRightMargin / 2,
                theme.getLeftY() - lineMaxLength / 10 * 3,
                theme.getLeftX() - nodeLeftRightMargin / 2,
                theme.getLeftY() - lineMaxLength / 10,
                theme.getLeftX(),
                theme.getLeftY()
            )
        } else {
            linePath.moveTo(child.getLeftX(), child.getLeftY())
            val lineMaxLength = theme.getRightY() - child.getRightY()
            linePath.cubicTo(
                child.getLeftX(),
                child.getLeftY(),
                child.getLeftX() - nodeLeftRightMargin / 2,
                child.getLeftY() + lineMaxLength / 10,
                child.getLeftX() - nodeLeftRightMargin / 2,
                child.getLeftY() + lineMaxLength / 10 * 3
            )
            linePath.cubicTo(
                theme.getRightX() + nodeLeftRightMargin / 2,
                theme.getRightY() - lineMaxLength / 10 * 3,
                theme.getRightX() + nodeLeftRightMargin / 2,
                theme.getRightY() - lineMaxLength / 10,
                theme.getRightX(),
                theme.getRightY()
            )
        }
        canvas?.drawPath(linePath, lineRectPaint)
    }

    fun setData(nodeList: ArrayList<MapNode>) {
        nodeList.find { it.isTheme }?.let { calculationThemeNode(it.nodeName, it.hasSubNode) }
        val leftChildNode = ArrayList<MapNode>()
        val rightChildNode = ArrayList<MapNode>()
        nodeList.filterNot { it.isTheme }.forEachIndexed { index, mapNode ->
            if (index % 2 == 0) {
                leftChildNode.add(mapNode)
            } else {
                rightChildNode.add(mapNode)
            }
        }
        calculationLeftChildNode(leftChildNode)
        calculationRightChildNode(rightChildNode)
        invalidate()
    }

    private fun calculationThemeNode(nodeName: String, hasChild: Boolean) {
        val themeXMindNode = XMindNode(
            Rect(
                viewWidth / 2 - themeSize.width / 2,
                viewHeight / 2 - themeSize.height / 2,
                viewWidth / 2 + themeSize.width / 2,
                viewHeight / 2 + themeSize.height / 2
            ), nodeName, hasChild
        )
        themeNode = ThemeNode(context, themeXMindNode)
    }

    private fun calculationRightChildNode(rightChildNode: ArrayList<MapNode>) {
        val startX =
            (viewWidth / 2 + themeSize.width / 2 + nodeLeftRightMargin + childNodeSize.width)
        if (rightChildNode.size % 2 == 1) {
            calculationChildNodeOdd(startX, rightChildNode, false)
        } else {
            calculationModelNodeEven(startX, rightChildNode, false)
        }
    }

    private fun calculationModelNodeEven(
        startX: Int,
        childNode: java.util.ArrayList<MapNode>,
        isLeft: Boolean
    ) {
        childNode.forEachIndexed { index, mapNode ->
            if (index % 2 == 0) {
                val rect = Rect(
                    startX - childNodeSize.width,
                    viewHeight / 2 - nodeTopBottomMargin - childNodeSize.height
                            - (2 * nodeTopBottomMargin + childNodeSize.height) * (index / 2),
                    startX,
                    viewHeight / 2 - nodeTopBottomMargin - (2 * nodeTopBottomMargin + childNodeSize.height) * (index / 2)
                )
                val node = XMindNode(rect, mapNode.nodeName, mapNode.hasSubNode)
                childNodeList.add(ChildNode(context, node, isLeft))
            } else {
                val rect = Rect(
                    startX - childNodeSize.width,
                    viewHeight / 2 + nodeTopBottomMargin + (2 * nodeTopBottomMargin + childNodeSize.height) * (index / 2),
                    startX,
                    viewHeight / 2 + nodeTopBottomMargin + childNodeSize.height + (2 * nodeTopBottomMargin + childNodeSize.height) * (index / 2)
                )
                val node = XMindNode(rect, mapNode.nodeName, mapNode.hasSubNode)
                childNodeList.add(ChildNode(context, node, isLeft))
            }
        }
    }

    private fun calculationChildNodeOdd(
        startX: Int,
        childNode: java.util.ArrayList<MapNode>,
        isLeft: Boolean
    ) {
        childNode.forEachIndexed { index, mapNode ->
            if (index == 0) {
                val rect = Rect(
                    startX - childNodeSize.width,
                    viewHeight / 2 - childNodeSize.height / 2,
                    startX,
                    viewHeight / 2 + childNodeSize.height / 2
                )
                val node = XMindNode(rect, mapNode.nodeName, mapNode.hasSubNode)
                childNodeList.add(ChildNode(context, node, isLeft))
            } else {
                if (index % 2 == 0) {
                    val rect = Rect(
                        startX - childNodeSize.width,
                        viewHeight / 2 - childNodeSize.height / 2 + (2 * nodeTopBottomMargin + childNodeSize.height) * (index / 2),
                        startX,
                        viewHeight / 2 + childNodeSize.height / 2 + (2 * nodeTopBottomMargin + childNodeSize.height) * (index / 2)
                    )
                    val node = XMindNode(rect, mapNode.nodeName, mapNode.hasSubNode)
                    childNodeList.add(ChildNode(context, node, isLeft))
                } else {
                    val rect = Rect(
                        startX - childNodeSize.width,
                        viewHeight / 2 - childNodeSize.height / 2 - (2 * nodeTopBottomMargin + childNodeSize.height) * ((index + 1) / 2),
                        startX,
                        viewHeight / 2 + childNodeSize.height / 2 - (2 * nodeTopBottomMargin + childNodeSize.height) * ((index + 1) / 2)
                    )
                    val node = XMindNode(rect, mapNode.nodeName, mapNode.hasSubNode)
                    childNodeList.add(ChildNode(context, node, isLeft))
                }
            }
        }
    }

    private fun calculationLeftChildNode(leftChildNode: ArrayList<MapNode>) {
        val startX = (viewWidth / 2 - themeSize.width / 2 - nodeLeftRightMargin)
        if (leftChildNode.size % 2 == 1) {
            calculationChildNodeOdd(startX, leftChildNode, true)
        } else {
            calculationModelNodeEven(startX, leftChildNode, true)
        }
    }

    inner class XMindNodeSize(var width: Int, var height: Int)
}

本文地址:https://blog.csdn.net/wanggang514260663/article/details/112597482