Android自定义view、动手实现一个简单的xmind思维导图结构
老样子,先放效果图吸引火力
1、思路拆分
猛一看这个图,感觉有种无从小手的感觉是不是?
那么让我先来拆分下思路,相信你会觉得很简单。
首先这个思维导图构成是由
主题:
子节点:
其中子节点支持控制都是还有子节点
链接线:
链接线是支持弧度的,第一感觉就是要用贝塞尔曲线来实现。
2、细分实现
其实观察下来,实现上面的难点主要是两个,第一个是文字换行,第二个是贝塞尔曲线弧度。下面我来一个个拆解下。
2-1、文字换行实现思路
如果是熟悉自定义view的同学,肯定知道单纯的canvas.drawText
是不能实现文字换行的。你一定会说textview也是一个自定义view啊,为啥人家可以?如果你看过textview的源码,你一定会发现textview的所有核心操作,都是在三个layout中,分别是boringlayout
、staticlayout
、dynamiclayout
。这三个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)
}
本文地址:https://blog.csdn.net/wanggang514260663/article/details/112597482