上一篇以从左到右方向为例讲解了树形布局中子控件的摆放,本篇讲解结点间连接线的绘制。
前期准备
一般情况下布局类控件其实更关注于子控件的测量和摆放(例如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项目看看代码实现。
下一篇讲解事件拦截与拖拽效果的实现。