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


在这里插入图片描述

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

1、思路拆分

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

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

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

主题:
在这里插入图片描述
子节点:
在这里插入图片描述
其中子节点支持控制都是还有子节点

链接线:
在这里插入图片描述
链接线是支持弧度的,第一感觉就是要用贝塞尔曲线来实现。

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、贝塞尔曲线弧度实现思路

在这里插入图片描述
其实核心就是对两端进行贝塞尔曲线处理,完了链接对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)
}
  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 12
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值