查看原文
其他

小知识又来了!ViewGroup onDraw为什么不调用?

fishforest 鸿洋 2021-10-12

本文作者


作者:fishforest

链接:

https://www.jianshu.com/p/1edd7f7db65e

本文由作者授权发布。


多数同学应该都知道 ViewGroup 想要 onDraw 执行的时候得设置 setWillNotDraw(false),但是有没有深入过底层的原理呢?一起来学习吧。


前言


通过本篇文章,你将了解到:


1、ViewGroup onDraw不执行的原因。


2、怎么让
ViewGroup onDraw执行。


3、
setWillNotDraw(boolean)作用。


如果对原理不感兴趣,请拉到最后的总结查看解决办法~


小例子


我们知道自定义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,100100);
    }

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawRect(rect, paint);
    }
}


效果图:



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


现在自定义一个
ViewGroupMyFrameLayout,并把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(00200200);
    }

    @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>


来看看效果:



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


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


2、
MyFrameLayoutonDraw()没有调用。


寻根溯源


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


1、从根view(viewGroup)开始,先绘制自身,再绘制子view。


2、子view继续按照第一步递归。


3、如果当前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);

        //省略

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


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


硬件加速


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



[]表示该调用该类里的对应方法。


()表示方法里的参数


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


决定是否绘制的因素


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


软件绘制:


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

                ...
                // 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方法

                    //省略
                    // 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简单继承了FrameLayoutMyView简单继承了View,我们并没有对两者进行单独设置标记,进而猜测是viewGroup和View初始化时对于PFLAG_SKIP_DRAW标记做了不同的处理。


来看看
ViewGroup初始化:



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


vew.java setFlags方法

        //省略
        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);
        }
        //省略


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


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


2、如果没有设置
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标记,也就没有后面的事了。


如何让viewGroup onDraw()执行


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


先来看看
WILL_NOT_DRAW


view.java

    /**
     * If this view doesn't do any drawing on its own, set this flag to
     * allow further optimizations. By default, this flag is not set on
     * View, but could be set on some View subclasses such as ViewGroup.
     *
     * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
     * you should clear this flag.
     *
     * @param willNotDraw whether or not this View draw on its own
     */
    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }

    /**
     * Returns whether or not this View draws on its own.
     *
     * @return true if this view has nothing to draw, false otherwise
     */
    @ViewDebug.ExportedProperty(category = "drawing")
    public boolean willNotDraw() {
        return (mViewFlags & DRAW_MASK) == WILL_NOT_DRAW;
    }


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


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


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


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加上背景,再来看看效果:



蓝色是MyView绘制出来的。


红色是MyFrameLayout绘制出来的。


绿色是
MyFrameLayout设置的背景。


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


总结


若要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覆盖。




最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!



推荐阅读


中级Android研发,面试问什么?
实际经历来说说IdleHandler的坑
难得的Android 启动优化好文!


扫一扫 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!

: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存