重新复习Android View的绘制流程

PhoneWindow是Android系统中最基本的窗口系统,每个Activity会创建一个。同时,PhoneWindow也是Activity和View系统交互的接口。DecorView本质上是一个FrameLayout,是Activity中所有View的祖先。

开始:DecorView被加载到Window中

1.Activity里的PhoneWindow创建是在Activity的attach方法中,这个方法是Activity在ActivityThread被创建之后调用

Activity.java
在这里插入图片描述
ActivityThread.java
在这里插入图片描述
在这里插入图片描述
2.当Activity onResume时候,会给Activity的mDecor进行赋值
ActivityThread#handleResumeActivity
在这里插入图片描述
很明显这里的r.window是来自于activity的window,也就是activity的phonewindow的DecorView

而activity的phonewindow的DecorView是在activity的attach方法时进行赋值的
ActivityThread#performLaunchActivity
在这里插入图片描述
Activity.java
在这里插入图片描述
PhoneWindow.java
在这里插入图片描述

decorview是在activity的handleResumeActivity添加到Activity 中的,通过的是WindowManager的辅助
ActivityThread#handleResumeActivity
在这里插入图片描述
WindowManager的实现类是WindowManagerImpl,所以实际调用的是WindowManagerImpl的addView方法

ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成的。在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联,相关源码如下所示:

WindowManagerGlobal.java
在这里插入图片描述
ViewRootlmpl的setView方法会调用requestLayout,requestLayout方法里会出发scheduleTraversals,这个scheduleTraversals很多情况下都会调用,比如setLayoutParams,改变可见性等,里面就来到了performTraversals方法,主要绘制流程都在这个方法中

了解绘制的整体流程

绘制会从根视图ViewRoot的performTraversals()方法开始,从上到下遍历整个视图树,每个View控件负责绘制自己,而ViewGroup还需要负责通知自己的子View进行绘制操作。performTraversals()

private void performTraversals() {
    
    
    ...
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    ...
    //执行测量流程
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    ...
    //执行布局流程
    performLayout(lp, desiredWindowWidth, desiredWindowHeight);
    ...
    //执行绘制流程
    performDraw();
}

preformLayout和performDraw的传递流程和performMeasure是类似的,唯一不同的是,performDraw的传递过程是在draw方法中通过dispatchDraw来实现的,不过这并没有本质区别。

获取content:

ViewGroup content = (ViewGroup)findViewById(android.R.id.content);

理解MeasureSpec

MeasureSpec源码解析
MeasureSpec表示的是一个32位的整形值,它的高2位表示测量模式SpecMode,低30位表示某种测量模式下的规格大小SpecSize。MeasureSpec是View类的一个静态内部类,用来说明应该如何测量这个View。

MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,为了方便操作,其提供了打包和解包的方法,打包方法为makeMeasureSpec

     public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
    
    
            if (sUseBrokenMakeMeasureSpec) {
    
    
                return size + mode;
            } else {
    
    
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }

解包方法源码如下:

public static int getMode(int measureSpec) {
    
    
    return (measureSpec & MODE_MASK);
}

public static int getSize(int measureSpec) {
    
    
    return (measureSpec & ~MODE_MASK);
}

结论:对于DecorView而言,它的MeasureSpec由窗口尺寸和其自身的LayoutParams共同决定;对于普通的View,它的MeasureSpec由父视图的MeasureSpec和其自身的LayoutParams共同决定。

View绘制流程之Measure

Measure的基本流程
页面的测量流程是从performMeasure方法开始的,相关的核心代码流程如下。
performMeasure属于ViewRootImpl,这里的mView就是通过setView方法设置进来的,在Activity中调用setView设置给ViewRootImpl就是DecorView,所以perormMeasure调用的是DecorView的measure方法,可见ViewRootImpl并不是一个view,只是根view的代理类,实现一些根view具备的方法

private void perormMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    
    
    ...
    // 具体的测量操作分发给ViewGroup
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    ...
}

// 在ViewGroup中的measureChildren()方法中遍历测量ViewGroup中所有的View
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    
    
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
    
    
        final View child = children[i];
        // 当View的可见性处于GONE状态时,不对其进行测量
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
    
    
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

// 测量某个指定的View
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
    
    
    final LayoutParams lp = child.getLayoutParams();

    // 根据父容器的MeasureSpec和子View的LayoutParams等信息计算
    // 子View的MeasureSpec
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height);
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

// View的measure方法
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    
    
    ...
    // ViewGroup没有定义测量的具体过程,因为ViewGroup是一个
    // 抽象类,其测量过程的onMeasure方法需要各个子类去实现
    onMeasure(widthMeasureSpec, heightMeasureSpec);
    ...
}

// 不同的ViewGroup子类有不同的布局特性,这导致它们的测量细节各不相同,如果需要自定义测量过程,则子类可以重写这个方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
    
    // setMeasureDimension方法用于设置View的测量宽高
    setMeasureDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), 
    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

// 如果View没有重写onMeasure方法,则会默认调用getDefaultSize来获得View的宽高
public static int getDefaultSize(int size, int measureSpec) {
    
    
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    
    
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = sepcSize;
            break;
    }
    return result;
}

总结来说:
measure是View之中方法,其中会调用onMeasure进行测量,onMeasure在view之中有默认的实现,一般继承ViewGroup的类会重写这个方法来改变测量的规则, view的onMeasure会调用getDefaultSize来测量尺寸,也就是人们一般说自定义控件wrap_content会出现不生效问题的出处

对getSuggestMinimumWidth的分析

protected int getSuggestedMinimumWidth() {
    
    
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinmumWidth());
}

protected int getSuggestedMinimumHeight() {
    
    
    return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}

public int getMinimumWidth() {
    
    
    final int intrinsicWidth = getIntrinsicWidth();
    return intrinsicWidth > 0 ? intrinsicWidth : 0;
}

如果View没有设置背景,那么返回android:minWidth这个属性所指定的值,这个值可以为0;如果View设置了背景,则返回android:minWidth和背景的最小宽度这两者中的最大值。

自定义View时手动处理wrap_content时的情形
直接继承View的控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent。

public static int getDefaultSize(int size, int measureSpec) {
    
    
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    
    
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = sepcSize;
            break;
    }
    return result;
}

这里的measureSpec是当前测量view自己的measureSpec,子View的MeasureSpec值是根据子View的布局参数(LayoutParams)和父容器的MeasureSpec值计算得来,具体计算逻辑封装在getChildMeasureSpec()里。

Viewgroup的getChildMeasureSpec()遵循如下规则:
在这里插入图片描述
从上面可以看出,当子View的布局参数使用wrap_content或wrap_content时:

子View的specMode模式:AT_MOST
子View的specSize(宽 / 高):parenSize = 父容器当前剩余空间大小 = match_content

所以这就是为什么自定义控件wrap_content不生效的原因,解决方法如下:
复写onMeasure


 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
    

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);


        // 获取宽-测量规则的模式和大小
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        // 获取高-测量规则的模式和大小
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        // 设置wrap_content的默认宽 / 高值
        // 默认宽/高的设定并无固定依据,根据需要灵活设置
        // 类似TextView,ImageView等针对wrap_content均在onMeasure()对设置默认宽 / 高值有特殊处理,具体读者可以自行查看
        int mWidth = 400;
        int mHeight = 400;

      // 当布局参数设置为wrap_content时,设置默认值
        if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT && getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
    
    
            setMeasuredDimension(mWidth, mHeight);
        // 宽 / 高任意一个布局参数为= wrap_content时,都设置默认值
        } else if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
    
    
            setMeasuredDimension(mWidth, heightSize);
        } else if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
    
    
            setMeasuredDimension(widthSize, mHeight);
        }

在Activity中获取某个View的宽高
**由于View的measure过程和Activity的生命周期方法不是同步执行的,如果View还没有测量完毕,那么获得的宽/高就是0。所以在onCreate、onStart、onResume中均无法正确得到某个View的宽高信息。**解决方式如下:

Activity/View#onWindowFocusChanged

// 此时View已经初始化完毕
// 当Activity的窗口得到焦点和失去焦点时均会被调用一次
// 如果频繁地进行onResume和onPause,那么onWindowFocusChanged也会被频繁地调用
public void onWindowFocusChanged(boolean hasFocus) {
    
    
    super.onWindowFocusChanged(hasFocus);
    if (hasFocus) {
    
    
        int width = view.getMeasureWidth();
        int height = view.getMeasuredHeight();
    }
}

view.post(runnable)

// 通过post可以将一个runnable投递到消息队列的尾部,// 然后等待Looper调用次runnable的时候,View也已经初
// 始化好了
protected void onStart() {
    
    
    super.onStart();
    view.post(new Runnable() {
    
    

        @Override
        public void run() {
    
    
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    });
}

ViewTreeObserver

// 当View树的状态发生改变或者View树内部的View的可见// 性发生改变时,onGlobalLayout方法将被回调
protected void onStart() {
    
    
    super.onStart();

    ViewTreeObserver observer = view.getViewTreeObserver();
    observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
    
    

        @SuppressWarnings("deprecation")
        @Override
        public void onGlobalLayout() {
    
    
            view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    });
}

View.measure(int widthMeasureSpec, int heightMeasureSpec)

View的绘制流程之Layout

Layout的基本流程

// ViewRootImpl.java
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {
    
    
    ...
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    ...
}

// View.java
public void layout(int l, int t, int r, int b) {
    
    
    ...
    // 通过setFrame方法来设定View的四个顶点的位置,即View在父容器中的位置
    boolean changed = isLayoutModeOptical(mParent) ? 
    set OpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    ...
    onLayout(changed, l, t, r, b);
    ...
}

// 空方法,子类如果是ViewGroup类型,则重写这个方法,实现ViewGroup
// 中所有View控件布局流程
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    
    

}

同样这里调用decorview的layout方法,里面又onLayout方法,由各个控件自己实现

注意:在View的默认实现中,View的测量宽/高和最终宽/高是相等的,只不过测量宽/高形成于View的measure过程,而最终宽/高形成于View的layout过程,即两者的赋值时机不同,测量宽/高的赋值时机稍微早一些。在一些特殊的情况下则两者不相等:

View需要多长measure才能确定自己的测量宽/高,在前几次测量的过程中,其得出的测量宽/高有可能和最终宽/高不一致,但最终来说,测量宽/高还是和最终宽/高相同

View的绘制流程之Draw

private void performDraw() {
    
    
    ...
    draw(fullRefrawNeeded);
    ...
}

private void draw(boolean fullRedrawNeeded) {
    
    
    ...
    if (!drawSoftware(surface, mAttachInfo, xOffest, yOffset, 
    scalingRequired, dirty)) {
    
    
        return;
    }
    ...
}

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, 
int xoff, int yoff, boolean scallingRequired, Rect dirty) {
    
    
    ...
    mView.draw(canvas);
    ...
}

// 绘制基本上可以分为六个步骤
public void draw(Canvas canvas) {
    
    
    ...
    // 步骤一:绘制View的背景
    drawBackground(canvas);

    ...
    // 步骤二:如果需要的话,保持canvas的图层,为fading做准备
    saveCount = canvas.getSaveCount();
    ...
    canvas.saveLayer(left, top, right, top + length, null, flags);

    ...
    // 步骤三:绘制View的内容
    onDraw(canvas);

    ...
    // 步骤四:绘制View的子View
    dispatchDraw(canvas);

    ...
    // 步骤五:如果需要的话,绘制View的fading边缘并恢复图层
    canvas.drawRect(left, top, right, top + length, p);
    ...
    canvas.restoreToCount(saveCount);

    ...
    // 步骤六:绘制View的装饰(例如滚动条等等)
    onDrawForeground(canvas)
}

setWillNotDraw的作用

// 如果一个View不需要绘制任何内容,那么设置这个标记位为true以后,
// 系统会进行相应的优化。
public void setWillNotDraw(boolean willNotDraw) {
    
    
    setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}

默认情况下,View没有启用这个优化标记位,但是ViewGroup会默认启用这个优化标记位。
当我们的自定义控件继承于ViewGroup并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化。
当明确知道一个ViewGroup需要通过onDraw来绘制内容时,我们需要显示地关闭WILL_NOT_DRAW这个标记位。

猜你喜欢

转载自blog.csdn.net/u012124438/article/details/113826817