Android View 工作原理基础

View 工作原理基础

本章主要介绍View的工作原理,了解了工作原理后才能作出比较完善的自定义View,View需要掌握三大流程:measure、layout和draw,除了三大流程以外,还需要掌握常见的回调方法,比如构造方法、onAttach、onVisibilityChanged、onDetach等。

初识ViewRoot和DecorView

了解ViewRoot和DecorView的概念有助于更好理解三大流程。

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

root = new ViewRootImpl(view.getContext(),display);
root.setView(view,wparams,panelParentView);

View的绘制流程从ViewRoot的performTraversals方法开始,然后经过measure,layout和draw三个过程才能将View绘制出来,其中measure测量View的宽高,layout确定view在父容器中的位置,draw负责将View绘制在屏幕上,performTraversals的大致流程图如下:

在这里插入图片描述

performTraversals会依次调用performMeasure、performLayout、performDraw,这三个方法分别完成顶级View的measure、layout和draw三大流程。其中在performMeasure中会调用measure方法,在measure方法中又调用onMeasure,onMeasure方法中会对所有子元素进行measure,这个时候measure流程就从父容器传递到子元素了,这样就完成了一次measure,接着子元素会重复父容器的measure过程,如此反复的完成了整个View树的遍历。其他两个同理,唯一不同的是performDraw的传递过程是在draw方法中通过dispatchDraw来实现的,不过这并没有什么本质区别。

measure过程决定View的宽高,Measure完成后可以通过getMeasureWidth和getMeasureHeight来获取View测量后的宽/高,处特殊情况外等同与View最终的宽高。layout过程决定了view的四个顶点的坐标和实际View的宽/高,layout完成后可以通过getTop、getLeft、getRight和getBottom获得四个顶点的位置,并可以通过getWidth和getHeight拿到View最终宽/高。Draw决定了View的显示,只有draw方法完成以后,view才会显示在屏幕上。

如下图所示,DecorView作为顶级View,一般情况下包含一个竖直方向的LinearLayout,分为上下两部分,上面是标题栏,下面是内容栏。在Activity中,通过setContentView设置的布局文件就是放在内容栏中,而内容栏的id则是content,因此Activity指定布局的方法是setContentView。可以通过ViewGroup content = findviewbyid(android.R.id.content)获取content,通过content.getChildAt(0)获取设置的View。同时通过源码可知,DecorView是一个FrameLayout,View层事件都先经过DecorView,然后才传递给View。

在这里插入图片描述

理解MeasureSpec

为了更好的理解View的测量过程,需要理解MeasureSpec,MeasureSpec参与了View的测量过程,在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后再根据这个measureSpec来测量出View的宽高。

MeasureSpec

MeasureSpec代表一个32位int值,高2位代表SpecMode测量模式,低30位代表SpecSize规格大小,下面先看一下MeasureSpec内部的一些常量定义:

private static final int MODE_SHIFT = 30;
private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY     = 1 << MODE_SHIFT;
public static final int AT_MOST     = 2 << MODE_SHIFT;

public static int makeMeasureSpec(int size, 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);
}

MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,一组SpecMode和specSize可以打包成一个MeasureSpec值,一个MeasureSpec值也可以解包出原始的SpecMode和SpecSize。

SpecMode有三类:

  • UNSPECIFIED
    父容器不对View有任何的限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态
  • EXACTLY
    父容器已经检测出View所需要的精度大小,这个时候View的最终大小就是SpecSize所指定的值,它对应于LayoutParams中的match_parent和具体的数值
  • AT_MOST
    父容器指定了一个可用大小即SpecSize,view的大小不能大于这个值,具体是什么值要看不同view的具体实现,它对应于LayoutParams中的wrap_content

MeasureSpec 和 LayoutParams 的对应关系

在view测量的时候,系统会将layoutParams在父容器的约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定view测量后的宽高。顶级view(DecorView)和普通view的MeasureSpec转换过程略有不同,其MeasureSpec由窗口的尺寸和其自身的LayoutParams来决定,对于普通View,其MeasureSpec由父容器的MeasureSpec和其自身的LayoutParams来决定。

对于DecorView来说,在ViewRootImpl中的measureHierarchy方法中展示了DecorView的MeasureSpec的创建过程,其中desiredWindowWidth和desiredWindowHeight是屏幕的尺寸:

 childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
 childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
    }
    return measureSpec;
}

通过上述代码,DecorView的MeasureSpec的产生过程就很明确了,具体来说其遵守了如下格式,根据LayoutParams的宽高的参数来划分

  • LayouParams.MATCH_PARENT:精确模式,大小就是窗口的大小
  • LayouParams.WRAP_CONTENT:最大模式,大小不定,但是不能超出屏幕的大小
  • 固定大小(比如100dp):精确模式,大小为LayoutParams中指定的大小

对于普通View来说,这里是指我们布局中的View,View的measure过程由ViewGroup传递而来,先看下ViewGroup的measureChildWithMargins方法:

protected void measureChildWithMargins(View child,
    int parentWidthMeasureSpec, int widthUsed,
    int parentHeightMeasureSpec, int heightUsed) {
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

上述的方法会对子元素进行measure,在调用子元素的measure方法之前会通过getChildMeasureSpec方法得到子元素的MesureSpec。子元素的MesureSpec的创建和父容器的MeasureSpec和子元素的LayoutParams有关,此外还和view的margin及padding有关,具体可以看下ViewGroup的getChildMeasureSpec方法

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);

    int size = Math.max(0, specSize - padding);

    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
    // Parent has imposed an exact size on us
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size. So be it.
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent has imposed a maximum size on us
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            // Child wants a specific size... so be it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size, but our size is not fixed.
            // Constrain child to not be bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent asked to see how big we want to be
    case MeasureSpec.UNSPECIFIED:
        if (childDimension >= 0) {
            // Child wants a specific size... let him have it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size... find out how big it should
            // be
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size.... find out how
            // big it should be
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }
    //noinspection ResourceType
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

上述方法主要作用是根据父容器的MeasureSpec同时结合view本身的LayoutParams来确定子元素的MesureSpec,参数中的padding是指父容器中已占用的空间大小,因此子元素可以用的大小为父容器的尺寸减去pading,具体代码如下:

int specSize = MesureSpec.getSize(spec);
int size = Math.max(0,specSize - pading);

getChildMeasureSpec清楚展示了普通View的MeasureSpec的创建规则,更加清晰的理解getChildMeasureSpec的逻辑,这里提供一个表,表中对getChildMeasureSpec的工作原理进行了梳理:

父容器MeasureSpec
子元素LayoutParams
EXACTLY AT_MOST UNSPECIFIED
dp/px EXACTLY
childSize
EXACTLY
childSize
EXACTLY
childSize
match_parent EXACTLY
parentSize
AT_MOST
parentSize
UNSPECIFIED
0
wrap_content AT_MOST
parentSize
AT_MOST
parentSize
UNSPECIFIED
0
发布了174 篇原创文章 · 获赞 119 · 访问量 55万+

猜你喜欢

转载自blog.csdn.net/lj402159806/article/details/99632001
今日推荐