Android 面试—自定义ViewGroup默认不执行onDraw 0

1.问题

我们知道自定义view的时候会重写onDraw()方法,如下:

public class MyView extends View {

    private Paint paint;

    private Rect rect;

    private Bitmap bitmap;

    private Matrix matrix;

    public MyView(Context context) {

        super(context);

        init();

    }

    public MyView(Context context, @Nullable AttributeSet attrs) {

        super(context, attrs);

        init();

    }

    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {

        super(context, attrs, defStyleAttr);

        init();

    }

    private void init() {

        paint = new Paint();

        paint.setAntiAlias(true);

        paint.setColor(Color.BLUE);

        rect = new Rect(0,0,100, 100);

    }

    @Override

    public void draw(Canvas canvas) {

        super.draw(canvas);

    }

    @Override

    protected void onDraw(Canvas canvas) {

        canvas.drawRect(rect, paint);

    }

}

效果图:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

 蓝色的方块即是我们自定义MyView绘制的。

现在自定义一个ViewGroup:MyFrameLayout,并把MyView添加到MyFrameLayout里。

public class MyFrameLayout extends FrameLayout {

    private Paint paint;

    private RectF rectF;

    public MyFrameLayout(@NonNull Context context) {

        super(context);

        init();

    }

    public MyFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) {

        super(context, attrs);

        init();

    }

    public MyFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {

        super(context, attrs, defStyleAttr);

        init();

    }

    private void init() {

        addView(new MyView(getContext()));

        paint = new Paint();

        paint.setAntiAlias(true);

        paint.setColor(Color.RED);

        rectF = new RectF(0, 0, 200, 200);

    }

    @Override

    protected void dispatchDraw(Canvas canvas) {

        super.dispatchDraw(canvas);

    }

    @Override

    protected void onDraw(Canvas canvas) {

        canvas.drawRect(rectF, paint);

    }

}

MyFrameLayout 里重写了onDraw()方法,试图绘制一个红色的矩形,并且将MyView作为子view添加到MyFrameLayout里。最后将MyFrameLayout添加到xml作为activity布局文件:

    <com.fish.myapplication.MyFrameLayout

        android:id="@+id/myFrameLayout"

        android:layout_width="400dp"

        android:layout_height="400dp">

    </com.fish.myapplication.MyFrameLayout>

来看看效果:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

 想象中红色的矩形并没有显示出来,猜测一下原因:

①MyView遮挡了MyFrameLayout的绘制?这个可以排除了,因为MyView蓝色区域大小:100px * 100px,而MyFrameLayout红色区域大小:200px * 200px。

②MyFrameLayout的onDraw()没有调用。

2.源码分析

这里涉及到view的绘制流程,不进行深入阐述,大致挑重点说一下:

从根view(viewGroup)开始,先绘制自身,再绘制子view;子view继续按照第一步递归;如果当前view不是viewGroup,那么不会绘制子view(因为它没有孩子啊)

列出view绘制源码,对应上述步骤。

public void draw(Canvas canvas) {

    //省略

    boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;

    boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;

    if (!verticalEdges && !horizontalEdges) {

        // 绘制自身内容

        onDraw(canvas);

        // 绘制子view

        dispatchDraw(canvas);

        //……

        return;

    }

    //……

}

回到之前的试验,MyFrameLayout onDraw()没有执行,但是MyView onDraw()执行了,说明MyFrameLayout dispatchDraw()方法执行了,所以有理由相信MyFrameLayout draw()方法没有执行,只执行了MyFrameLayout dispatchDraw(),继续探究。

①硬件加速

现在Android默认开启硬件加速,什么是硬件加速呢?为了加快Android绘制速度,适当解放cpu资源,Android将一部分绘制放到gpu执行。而对应的Android里面的canvas,也分为是否支持硬件加速,因此绘制流程也有所差异,流程图简示如下:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

 ViewGroup中dispatchDraw()方法会调用view的draw()进行子view的绘制:

protected void dispatchDraw(Canvas) {

    for(int i = 0;i < childrenCount;i++){

        //……

      more |=drawChild(canvas,Child,drawingTime);

    }

}

protected boolean drawChild(Canvas canvas,View child,long drawingTime) {

    return child.draw(canvas,this,drawingTime);

}

从上图可以看出,不管是否开启硬件加速,都会经历“跳过绘制”的逻辑判断,而该判断的分支就决定了viewGroup的ondraw()方法是否执行。如果“跳过绘制”成立,那么调用dispatchDraw()方法,继而调用子view进行绘制(如果有子view)。如果“跳过绘制”不成立,那么调用draw(x1),该方法上面分析过了:会调用dispatchDraw()和ondraw()方法。

②决定是否绘制的因素

从代码上分析结果来看,初步符合我们的猜想:MyFrameLayout因为某种原因,跳过了绘制,只调用了dispatchDraw()方法,从而onDraw()方法没有得到执行,最终导致没有绘制自身的内容。接下来看看“跳过绘制”的判断依据。

软件绘制:

view.java  draw(x1,x2,x3)方法:

boolean draw(Canvas canvas,ViewGroup parent,long drawingTime) {

    ...

    // Fast path for layouts with no backgrounds

   if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {

       mPrivateFlags &= ~PFLAG_DIRTY_MASK;

       dispatchDraw(canvas);

   } else {

       draw(canvas);

   }

   ...

}

硬件加速:

view.java updateDisplayListIfDirty方法:

public RenderNode updateDisplayListIfDirty(){

    //……

    // Fast path for layouts with no backgrounds

    if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {

        dispatchDraw(canvas);

        drawAutofilledHighlight(canvas);

        if (mOverlay != null && !mOverlay.isEmpty()) {

            mOverlay.getOverlayView().draw( canvas);

        }

        if (debugDraw()) {

            debugDrawFocus(canvas);

        }

     } else {

            draw(canvas);

    }

   //……

}

可以看出不管是否支持硬件加速,其判断依据是通过PFLAG_SKIP_DRAW标记来确定的,现在就需要找到这个标记什么时候赋值与清空的。MyFrameLayout onDraw()没执行,而MyView onDraw()执行了,那么猜测MyFrameLayout设置了 PFLAG_SKIP_DRAW标记,MyView没有设置。MyFrameLayout简单继承了FrameLayout,MyView简单继承了View,我们并没有对两者进行单独设置标记,进而猜测是viewGroup和View初始化时对于PFLAG_SKIP_DRAW标记做了不同的处理。

来看看ViewGroup初始化:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

viewGroup初始化的时候,默认设置了WILL_NOT_DRAW,从字面意思来看是“不会绘制”标记,这个标记是否和PFLAG_SKIP_DRAW有联系呢?继续查看setFlags方法:

vew.java setFlags方法:

void setFlags(int flags,int mask) {

    //……

    if ((changed & DRAW_MASK) != 0) {

        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;

        }

        requestLayout();

        invalidate(true);

    }

    //……

}

到此处就比较明朗,将两个标记值联系起来了:

①如果设置了WILL_NOT_DRAW标记,那么继续检查background、foreground(mDrawable字段)、focusHighLight是否有值,如果三者任意一个设置了,那么将PFLAG_SKIP_DRAW标记清除,否则将该标记加上。

②如果没有设置WILL_NOT_DRAW标记,那么将PFLAG_SKIP_DRAW标记清除。

至此,我们知道了MyFrameLayout onDraw()方法没有执行的原因:viewGroup默认设置了WILL_NOT_DRAW标记,进而设置了PFLAG_SKIP_DRAW标记,而在绘制的时候通过判断PFLAG_SKIP_DRAW标记来决定是否调用MyFrameLayout draw(x)方法,最终调用onDraw()方法。而view默认没有设置WILL_NOT_DRAW标记,也就没有后面的事了。

3.解决办法

既然知道了MyFrameLayout没有绘制的原因,那么就有方法让它执行绘制流程。

先来看看WILL_NOT_DRAW:

view.java

public void setWillNotDraw(boolean willNotDraw) {

    setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);

}

public boolean willNotDraw() {

    return (mViewFlags & DRAW_MASK) == WILL_NOT_DRAW;

}

View类里暴露了设置WILL_NOT_DRAW标记的接口:

setWillNotDraw(boolean willNotDraw),可以在MyFrameLayout里使用setWillNotDraw(false)。

不想设置该标记也是可行的,前面说过即使设置了WILL_NOT_DRAW,后面还是有判断background、foreground、focusHighLight是否有值。

background:view背景

foreground(mDrawable字段):view前景

focusHighLight:view获得焦点时高亮

我们只要设置了其中一个值,PFLAG_SKIP_DRAW标记将会被清空。

来看看这三个值如何影响PFLAG_SKIP_DRAW标记

view.java

public void setBackgroundDrawable(Drawable background) {

    if (background != null) {

       if ((mPrivateFlags & PFLAG_SKIP_DRAW) != 0) {

            mPrivateFlags &= ~PFLAG_SKIP_DRAW;

            requestLayout = true;

        }

    }

}

public void setForeground(Drawable foreground) {

    if (foreground != null) {

        if ((mPrivateFlags & PFLAG_SKIP_DRAW) != 0) {

            mPrivateFlags &= ~PFLAG_SKIP_DRAW;

        }

    }

}

private void setDefaultFocusHighlight(Drawable highlight) {

    mDefaultFocusHighlight = highlight;

    mDefaultFocusHighlightSizeChanged = true;

    if (highlight != null) {

        if ((mPrivateFlags & PFLAG_SKIP_DRAW) != 0) {

            mPrivateFlags &= ~PFLAG_SKIP_DRAW;

        }

  }

}

探究了原理,我们来看看实际效果,现在给MyFrameLayout加上背景,再来看看效果:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

 蓝色是MyView绘制出来的

红色是MyFrameLayout绘制出来的

绿色是MyFrameLayout设置的背景

看得出来,MyFrameLayout内容已经绘制出来(红色区域)

4.总结

若要ViewGroup onDraw()执行,只需要setWillNotDraw(false)、设置背景、设置前景、设置焦点高亮,4个选项其中一项满足即可。

当然如果不想在MyFrameLayout onDraw里绘制,也可以重写MyFrameLayout dispatchDraw()方法,在该方法里绘制MyFrameLayout内容。

@Override

protected void dispatchDraw(Canvas canvas) {

    canvas.drawRect(rectF, paint);

    super.dispatchDraw(canvas);

}

@Override

protected void onDraw(Canvas canvas) {

    // canvas.drawRect(rectF, paint);

}

需要注意的是,super.dispatchDraw(canvas)要放到后边执行,不然子view内容会被MyFrameLayout覆盖。

猜你喜欢

转载自blog.csdn.net/zenmela2011/article/details/123917044