自定义布局之树形布局(五):绘制结点连接线

本文深入探讨了在树形布局中如何正确地绘制结点间的连接线,避免被子控件遮挡的问题。通过重写dispatchDraw方法,在子控件绘制完成后绘制连接线,确保其可见性。同时介绍了连接线绘制器的抽象类及其两个实现类,以适应不同风格的连接线需求。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

上一篇以从左到右方向为例讲解了树形布局中子控件的摆放,本篇讲解结点间连接线的绘制。

前期准备

一般情况下布局类控件其实更关注于子控件的测量和摆放(例如LinearLayout和RelativeLayout),而在绘制上做相对较少的事情,因为它们本身其实没有太多需要花的东西,这里不妨深入ViewGroup的draw方法看看它到底绘制了什么。

其实ViewGroup并没有重写draw方法,而是直接沿用View的draw方法。

public void draw(Canvas canvas) {
        ...
        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */

        // Step 1, draw the background, if needed
        ...
        drawBackground(canvas);
		...
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            onDraw(canvas);

            // Step 4, draw the children
            dispatchDraw(canvas);
            
            ...

            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);

            ...
            // we're done...
            return;
        }

其实注释里已经写得很清楚了,这里总结一下:
drawBackground方法绘制背景 —> 布局的onDraw方法 —> dispatchDraw方法调用子控件draw方法 —> onDrawForeground绘制前景。

Canvas绘制图像有这样的特点,后面绘制的图像会遮住前面绘制的图像。这也就是为什么背景图要最先绘制了。布局的onDraw方法调用比子控件的绘制要早,所以绘制结点连接线不应该在onDraw上进行。如下图,黄色部分的连接线会被遮盖住,非常不美观。
在这里插入图片描述
为了在绘制子控件后才绘制连接线,我们可以重写dispatchDraw方法,在完成View的全部绘制工作后才开始绘制连接线,这样就能避免连接线被遮住了。

重写dispatchDraw方法

代码如下,super.dispatchDraw方法完成了子控件的绘制,接着再绘制连接线就行了。

	@Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        onDrawConnectLine(canvas);
    }

具体实现如下。

	private LineDrawer mLineDrawer;
    private Rect mStartRect;
    private Rect mEndRect;

	/**
     * 绘制结点的连接线
     * @param canvas 绘制的画布
     */
    protected void onDrawConnectLine(Canvas canvas){
        View root = getChildAt(0);
        mStartRect.left = root.getLeft();
        mStartRect.right = root.getRight();
        mStartRect.top = root.getTop();
        mStartRect.bottom = root.getBottom();

        for(int i = 1;i < getChildCount();i++){
            View child = getChildAt(i);
            if(child.getVisibility() == View.GONE){
                continue;
            }
            mEndRect.left = child.getLeft();
            mEndRect.right = child.getRight();
            mEndRect.top = child.getTop();
            mEndRect.bottom = child.getBottom();            
           //1
           mLineDrawer.onDrawLine(canvas,mPaint,mStartRect,mEndRect,mTreeDirection);
        }
    }

mStartRect是指连接线起点所对应的控件,mEndRect是指连接线终点所对应的控件,复用Rect可以减少新对象的产生。

mLineDrawer是连接线绘制器,为了能绘制不同风格的连接线,TreeLayout提供了连接线绘制器的抽象类。

public static abstract class LineDrawer {
        /**
         * 连接线绘制器抽象类
         * @param canvas 绘制连接线的画布
         * @param paint 绘制连接线的画笔
         * @param start 连接线的起点控件的区域,即父结点控件所在区域
         * @param end 连接线的终点控件的区域,即子结点控件所在区域
         * @param direction 树的方向
         *                  参考{@link #DIRECTION_LEFT_TO_RIGHT,
         *                      @link #DIRECTION_RIGHT_TO_LEFT,
         *                      @link #DIRECTION_UP_TO_DOWN,
         *                      @link #DIRECTION_DOWN_TO_UP}
         */
        protected abstract void onDrawLine(Canvas canvas, Paint paint, Rect start, Rect end, int direction);
    }

版本问题

为什么不重写onDrawFroground方法,在这上面实现连接线绘制呢?其实上文提供的draw方法代码是Android sdk23的。

public void draw(Canvas canvas) {
        ...
        if (!verticalEdges && !horizontalEdges) {
           
            onDraw(canvas);

            dispatchDraw(canvas);
            ...
            //1
            onDrawForeground(canvas);
            ...
            return;
        }

再来看看Android sdk22的draw方法。

public void draw(Canvas canvas) {
        ...
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }
		...
        if (!verticalEdges && !horizontalEdges) {
            if (!dirtyOpaque) onDraw(canvas);

            dispatchDraw(canvas);
			
			//2
            onDrawScrollBars(canvas);
            ...
            return;
        }

可以看到23版的调用了onDrawForeground方法,而22版并没有定义onDrawForeground这个方法。

代码2的onDrawScrollBars方法,在23版里换成了onDrawForeground方法,里面调用了onDrawScrollBars方法。

既然onDrawScrollBars在22和23版本都有调用,重写它行不行呢?很遗憾,不行:(

因为它被final修饰了,是不能被重写的。

protected final void onDrawScrollBars(Canvas canvas) {
	...
}

最后

上面说到LineDrawer 连接线绘制器是一个抽象类,它有两个实现类分别叫 DirectLineDrawer 和 DocumentLineDrawer,效果如下:
在这里插入图片描述
感兴趣的朋友可以到Github项目看看代码实现。

下一篇讲解事件拦截与拖拽效果的实现。

### 如何在Android应用中实现思维导图显示 #### 使用现有库简化开发流程 为了快速构建具备高效性能的思维导图功能,在Android平台可以考虑采用已有的开源框架来减少自定义控件所需的时间成本。例如,ThinkMap是一个专门为Android设备优化过的开源项目[^2]。 该库提供了简单易用的应用程序接口(API),允许开发者仅需少量代码就能完成复杂树形结构图表的渲染工作。这使得即使是没有太多图形处理经验的新手也能迅速上手并集成到自己的应用程序当中去。 #### 自定义View绘制节点与连接线 如果希望拥有更高的定制化程度,则可以通过继承`View`类来自行编写逻辑以达到相同效果。对于每一个代表知识点或任务项的数据对象而言,都需要对应一个可视化的组件实例;而这些实例之间相互关联的部分则由线条表示出来形成完整的网络关系图谱。 当各个子view位置确定完毕之后就可以按照需求对其进行连线操作了[^4]: ```java // 假设已经获取到了两个需要相连结点对应的坐标(x1,y1),(x2,y2) Path path = new Path(); path.moveTo(x1, y1); path.lineTo(x2, y2); canvas.drawPath(path, paint); // canvas为当前绘图环境,paint设置样式属性如颜色宽度等 ``` 以上代码片段展示了最基础版本中的直线链接方式,实际应用场景下可能还需要考虑到弯曲路径、箭头指示等多种情况下的适配方案。 #### 动态加载与交互体验增强 为了让用户体验更加流畅自然,在展示较大规模的地图时建议分批次渐进式呈现内容而不是一次性全部读取渲染。此外还可以利用动画效果配合手势识别机制让用户能够方便地缩放平移浏览整个布局范围内的任意细节部分[^5]。 最后值得注意的是,虽然上述方法可以帮助实现在移动终端上面向用户的可视化表达形式,但在具体实施过程中仍有许多因素值得深入探讨研究,比如数据存储格式的选择、多端同步更新策略的设计等等。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值