《Android自定义控件入门到精通》文章索引 ☞ https://blog.youkuaiyun.com/Jhone_csdn/article/details/118146683
《Android自定义控件入门到精通》所有源码 ☞ https://gitee.com/zengjiangwen/Code
文章目录
View树的绘制流程
测量、布局、绘制是自定义控件的三大流程
自定义控件分为两种:
- 一种为继承至View,我们称之为自定义View
- 一种为继承至ViewGroup,我们称之为自定义ViewGroup(ViewGroup也是继承至View)
测量和布局我们先放一放,前面我们学了自定义View的绘制,这篇我们来了解下View树的绘制流程
View的绘图有两个相关的方法():
//View.java
protected void dispatchDraw(Canvas canvas) {}
protected void onDraw(Canvas canvas) {}
TestView继承至View:
public class TestView extends View {
private Paint mPaint;
public TestView(Context context) {
this(context, null);
}
public TestView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private void init() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setStrokeWidth(5);
mPaint.setStyle(Paint.Style.FILL);
}
@Override
protected void dispatchDraw(Canvas canvas) {
mPaint.setColor(Color.BLUE);
canvas.drawRect(new RectF(0, 0, 200, 200), mPaint);
}
@Override
protected void onDraw(Canvas canvas) {
mPaint.setColor(Color.RED);
canvas.drawRect(new RectF(100, 100, 300, 300), mPaint);
}
}
红色矩形在蓝色矩形下面,所以红色先画,蓝色后画,也就是先执行onDraw(),再执行dispatchDraw
TestView继承至ViewGroup:
public class TestView extends ViewGroup {
private Paint mPaint;
public TestView(Context context) {
this(context, null);
}
public TestView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private void init() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setStrokeWidth(5);
mPaint.setStyle(Paint.Style.FILL);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
@Override
protected void dispatchDraw(Canvas canvas) {
mPaint.setColor(Color.BLUE);
canvas.drawRect(new RectF(0, 0, 200, 200), mPaint);
}
@Override
protected void onDraw(Canvas canvas) {
mPaint.setColor(Color.RED);
canvas.drawRect(new RectF(100, 100, 300, 300), mPaint);
}
}
红色并没有执行,而只执行了dispatchDraw方法,为什么呢?那就要研究下View树的绘制流程了。
啥是View树?
可以看到,我们的布局是一种嵌套的树结构,我们称之为View树,View树最顶层View(如我们这里最外层的LinearLayout)我们称之为root
首先系统会调用root的draw()方法开始View树的绘制流程:
//View.java
@CallSuper
public void draw(Canvas canvas) {
......
drawBackground(canvas);
......
onDraw(canvas);
......
dispatchDraw(canvas);
}
在root的绘制过程中,先调用了onDraw()绘制root本身的内容,再调用dispatchDraw()
那这时候就分两种情况了
情况一,若root是继承至View(View树只有root一个节点),那么onDraw()和dispatchDraw()就如上面自定义View所示,只有先后的区别,因为这两个方法在View.java中是两个空方法
情况二,若root是继承至ViewGroup(View树除了root外,可能还有root的子View),那么onDraw()还是onDraw(),因为ViewGroup并没有实现onDraw(),所以它还是一个空方法,而dispatchDraw()不一样了,ViewGroup对它进行了实现
//ViewGroup.java
@Override
protected void dispatchDraw(Canvas canvas) {
......
for (int i = 0; i < childrenCount; i++) {
drawChild(canvas, transientChild, drawingTime);
}
......
}
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
可以看到,在ViewGroup的dispatchDraw()方法中,通过遍历的方式通知子View去调用子View的draw()方法,通过这样的递归方式,完成了整个View树的绘制。
child.draw(canvas, this, drawingTime)
//View.java
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
......
// Fast path for layouts with no backgrounds
//当前child为ViewGroup且没有设置背景时
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
dispatchDraw(canvas);
} else {//否则
draw(canvas);
}
......
}
//设置mPrivateFlags,用来表示当前child是View还是ViewGroup,且是否有mBackground
void setFlags(int flags, int mask) {
......
if ((mViewFlags & WILL_NOT_DRAW) != 0) {
if (mBackground != null
|| mDefaultFocusHighlight != null
|| (mForegroundInfo != null && mForegroundInfo.mDrawable != null)) {
mPrivateFlags &= ~PFLAG_SKIP_DRAW;
} else {
mPrivateFlags |= PFLAG_SKIP_DRAW;
}
} else {
mPrivateFlags &= ~PFLAG_SKIP_DRAW;
}
......
}
可以看到draw()中的if-else语句:
-
如果当前child是ViewGroup并且没有设置背景,那么就直接调用dispatchDraw()去通知它的子View开始绘制
-
如果当前child是ViewGroup且设置了背景或者当前child是View,那么就调用draw()方法,那么在draw()方法中,就会依次调用下面三个方法:
drawBackground(canvas); onDraw(canvas); dispatchDraw(canvas);
到此,明白为什么自定义ViewGroup中的onDraw()方法没执行了吗?给它设置个背景就好了!
<cn.code.code.wiget.TestView
android:id="@+id/testView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"/>
我们给它添加了 background="#ffffff"之后,onDraw()和dispatchDraw()就都起作用了
那么针对自定义ViewGroup,onDraw()和dispatchDraw()方法我们该怎么去用呢?
onDraw()
我们可以看看LinearLayout中,它用onDraw()干了啥:
@Override
protected void onDraw(Canvas canvas) {
if (mDivider == null) {
return;
}
if (mOrientation == VERTICAL) {
drawDividersVertical(canvas);
} else {
drawDividersHorizontal(canvas);
}
}
可以看到,LinearLayout中,在onDraw()方法中实现了分割线的绘制。
dispatchDraw()
控制子View的绘制逻辑
@Override
protected void dispatchDraw(Canvas canvas) {
if (canDraw){
//甚至你觉得viewgroup的dispatchDraw()实现的不好,你可以在这里实现你自己的绘制逻辑
super.dispatchDraw(canvas);
}else{
mPaint.setColor(Color.RED);
mPaint.setTextSize(24);
canvas.drawText("你不能把你的子View画出来,因为你不让",20,20,mPaint);
}
}
用思维导图画了一下View树的绘制流程: