View的工作原理小结

View的工作原理

一、初识ViewRoot和DecorView

ViewRoot对应于ViewRootImpl类,为连接WindowManager和DecorView的纽带。首先看看ViewRootImpl是什么时候被调用的。

1、启动Acitivity 最终会调用 ActivityThread#handleResumeActivity(),这里面会执行窗体添加 DecorView 的操作

( ViewManager wm = a.getWindowManager();
    wm.addView(decor, l);//这是窗体添加 DecorView)

2、接下来可以看第一点里面的addView方法

mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);

可见在addView方法中又调用了mGlobal的addView方法,mGlobal为WindowManagerGlobal 的实例。

3、接下来可看mGolbal的addView方法

root = new ViewRootImpl(view.getContext,display);
try {
            root.setView(view, wparams, panelParentView);//由它去设置view
        } catch (RuntimeException e) {}
通过

上述方法我们可得知,addView最后还是调用ViewRootImpl来实现将DecorView 添加到Window上。

DecorView:作为顶级View,一般情况下它内部会包含一个竖直方向的LinearLayout,上面是标题栏,下面是内容栏。在Activity中我们通过setContentView所设置的布局文件其实就是被加到内容栏之中的ViewGroup

content = findViewById(R.android.id.content)//获取内容栏
content.getChildAt(0);//获取内容栏里面的子元素

|

二、初识MeasureSpec:

MeasureSpec很大程度上决定了一个View的尺寸规格,其本身为一个32为的int值,高2位为SpecMode(测量模式),低30为SpecSize(某种测量模式下的规格大小)。
其中SpecMode主要有三种:
一、UNSPECIFIED:父容器不对view有任何限制,表一种测量状态,应用于系统内部。
二、EXACTLY:精确大小,对应LayoutParamas中的match_parent和具体数值两种形式。
三、AT_MOST:父容器指定一个可用大小,对应于LayoutParams的wrap_content。

MeasureSpec的产生:系统将LayoutParams在父容器的约束下转换成对应的MeasureSpec。
(1)普通view的MeasureSpec:由父容器的MeasureSpec和自身的LayoutParams来共同决定,此外还和View的margin及padding有关。
普通view的MeasureSpec的创建规则如下表
在这里插入图片描述
(2)DecorView的MeasureSpec:由窗口的尺寸和其自身的LayoutParams来共同决定,其MeasureSpec在ViewRootImpl中的measureHierarchy方法中决定。

三、View的工作流程(measure、layout、draw)

1、measure
(1)view的measure:
view的measure方法是final类型的方法,子类不能去重写,在measure中调用了onMeasure方法,所以只需要关注onMeasure的实现就行。
流程: view —》measure() —》onMeasure —》setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(),widthMeasure),getDefaultSize(getSuggestedMinimumHeight(),HeightMeasure)){设置view宽高的测量值)}—》 getDefaultSize()

以下代码为getDefaultSize方法的实现:

    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;//这个size为getSuggestedMinimumWidth()传入的值,下面有这个方法
                   //具体实现的代码
    break;
    case MeasureSpec.AT_MOST://AT_MOST和.EXACTLY这两种情况。简单地理解,其实
    case MeasureSpec.EXACTLY://getDefaultSize返回的大小就是measureSpec中的
    result = specSize;       //的specSize,而这个specSize就是View测量后的大小
    break;
    }
    return result

以下代码为getSuggestedMinimumWidth()方法的实现(这个方法主要针对UNSPECIFIED模式):

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

根据上述方法我们可得知View没有设置背景,那么View的宽度为mMinWidth,而mMinWidth对android:minWidth这个属性所指定的值。
注意:通过getDefaultSize这个方法的实现可得知,如果直接继承View的自定义控件没有设置wrap_content的自身大小,那么在布局中使用wrap_content和使用match_parent并无区别
在这里插入图片描述
解决方法:其实说到底就是当为AT_MOST,也就是wrap_content,需要给其指定一个默认的宽高(mWidth、mHeight)。

protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth,mHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth,heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize,mHeight);
}
}

(2)ViewGroup的measure过程:没有重写View的onMeasure方法,提供了一个measureChildren方法。
measureChildren方法的具体实现:

 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];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child,widthMeasureSpec,heightMeasureSpec);
        }
    }
}

可见最后又调用measureChild方法。
measureChild的具体实现:

protected void measureChild(View child,int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
    mPaddingLeft + mPaddingRight,lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
    mPaddingTop + mPaddingBottom,lp.height);
    child.measure(childWidthMeasureSpec,childHeightMeasureSpec);
}

ViewGroup是抽象类,其不对onMeasure方法进行统一实现的原因是,不同的ViewGroup子类有不同的布局特性,比如(LinearLayout和RelativeLayout)。
上述已经总结完View的大致测量流程。
接下来记录下一些注意点:
tip:什么时候去获取一个view的宽高(因为view的measure和Activity的生命周期方法不是同步执行的)?
解决办法:
1、在onWindowFocusChanged中去获取,这个时候可保证view已经绘制完毕,Activity窗口得到焦点和失去焦点都会被调用一次。
2、view.post(runnable),将runnable放到消息队列的尾部,Looper调用此runnable,view也初始化了。
3、viewTreeObserver,其中许多回调可以获取view的宽高,当View树的状态以及可见性改变时会回调onGlobalLayout方法。

2、layout(即有测量、必有布局):
(1)View的layout:
首先看下view的layout方法(伪代码):

public void layout(int l,int t,int r,int b){
    //初始化mLeft、mright、mTop、mBottom四个值
    //顶点确定,位置也就确定了
    //setFrame()
}

(2)ViewGroup的layout
View和ViewGroup均没有实现onLayout方法,因为其的具体实现也是和布局特性有关的。
流程:父元素的layout(完成自己的定位) --》 父元素的onLayout --》setChildFrame(为子元素指定对应的位置,其实就是调用子元素的layout方法而已) --》 子元素的layout --》 setFrame;

3Draw
绘制过程:绘制背景 – 》绘制本身 --》绘制children(dispatchDraw)–》绘制装饰;

四、自定义View

1、自定义view的四种类型:
(1)直接继承View注意点:
需要重写onDraw方法,自己支持wrap_content、padding也需要自己处理。

(2)直接继承viewgroup注意点:
处理好measure和layout方法,同时需要考虑padding和margin的影响,不然本身padding和子元素的margin会失效

(3)继承特定的view(例如button)

(4) 继承特定的LinearLayout(例如LinearLayout)
2、自定义属性
步骤:
(1)values文件夹下创建attrs.xml<? xml version="1.0" encoding="utf-8"?>

<resource>
    <declare-styleble name="CircleView">
        <attr name="circle_color" format="color" />
     </decalare-styleable>
<resource>        

(2) 代码中获取此属性//view的构造方法里面

TypeArray a = context.obtainStyledAttributes(attrs,R.styleable.CircleView);
mColor = a.getColor(styleable.CircleView_circle_color,Color.RED);
a.recyle();

(3)使用自定义属性,需要在布局文件中声明:xmlns:app=http://schemas.android.com/apk/res-auto

自定义view是一个比较复杂的事,这里只是粗略的记录下一些注意点.

发布了14 篇原创文章 · 获赞 45 · 访问量 2456

猜你喜欢

转载自blog.csdn.net/weixin_42683077/article/details/99169387