开发艺术探索--View的工作原理

第四章,View的工作原理

本章主要介绍两方面的内容
1. View的工作原理
2. 自定义View的实现方式

需要掌握:View的三大流程;View的常见回调方法;View滑动(上一章中的滑动冲突处理)

大纲

ViewRoot 和 DecorView
MeasureSpec
View工作流程
自定义View

初识ViewRoot 和 DecorView

ViewRoot的实现类是ViewRootImpl,是链接WindowManagerDecorView的纽带,View的三大流程都是通过ViewRoot来完成的

一. ViewRoot

View的绘制流程是从ViewRootperformTraversals开始的.

//这里面的三大流程中,前面的方法会调用后面的方法,ps:performMeasure会调用measure,measure会调用onMeasure
ViewRoot.performTraversals ->
(performMeasure)measure(onMeasure) ->
(performLayout)layout(onLayout)-> 
(performDraw)draw(onDraw)
  1. 其中measure用来测量View的宽和高,measure后即可获取到View测量的宽高.
  2. layout用来确定View在父容器中的放置位置
  3. draw则负责将View绘制在屏幕上,draw会调用dispatchDraw来对子View进行draw,只有draw方法完成后View才能显示在屏幕上.

二. DecorView

  1. DecorView是一个FrameLayout
  2. DecorView是一个顶级View,一般里面包含一个LinearLayout,上面的是标题栏,下面的是内容栏.
  3. setContentView就是将布局文件设置到内容区(id为android.R.id.contentFrameLayout中);

理解MeasureSpec

MeasureSpec 有点像测量规格或者测量说明书,View的尺寸和规格受MeasureSpec父容器 影响.

系统会将ViewLayoutParams根据父容器所施加的规则转换成对应的MeasureSpec

MeasureSpec

  1. 一个32位的int值,SpecMode(高2位),SpecSize(低30位).
  2. 有三类SpecMode,UNSPECIFIED(没有限制),EXACTLY(精确性),AT_MOST(不能大于这个值)

MeasureSpec与LayoutParams的关系

对于DecorView,其MeasureSpec窗口尺寸自身的LayoutParams共同决定.

  1. DecorViewMeasureSpec创建过程,可以查看ViewRootImpl#measureHierarchy,其中会调用ViewRootImpl#getRootMeasureSpec,
  2. EXACTLY模式下,DecorView大小就是窗口大小
  3. AT_MOST模式下,DecorView大小不定,不超过窗口大小
  4. 固定大小(EXACTLY),大小为LayoutParams中指定的大小.

对于普通View,其MeasureSpec父容器的MeasureSpec自身的LayoutParams共同决定.

  1. 查看ViewGroup#measureChildWithMargins,其中会调用ViewGroup#getChildMeasureSpec得到子元素的MeasureSpec.
  2. 子元素的MeasureSpec与父容器的MeasureSpec和本身的LayoutParamsView的Margin与Padding有关.

getChildMeasureSpec

View工作流程

一. measure 过程

View的measure过程

//View#onMeasure
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
  • 一般情况下getDefaultSize得到的就是View测量后的大小.
  • setMeasuredDimension()方法调用之后,我们才能使用getMeasuredWidth()getMeasuredHeight()来获取视图测量出的宽高,以此之前调用这两个方法得到的值都会是0.

  • View的最终大小在onLayout中确定,而测量大小onMeasure中确定,大多数情况下他们是相等的

  • getDefaultSize一般情况下,返回的是测量后的大小,在UNSPECIFIED模式下才返回getSuggestedMinimumWidth()getSuggestedMinimumHeight()

    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 = specSize;
            break;
        }
        return result;
    }
  • getSuggestMinimumWidth对应如下情况:

    1. view没背景则对应android:minWidth,如果此属性没指定,则为0
    2. view有背景则是minWidth(上面属性对应的值)和 background的minimumWidth的最大值,
    3. 通过Drawable#getMinimumWidth看出background的minimumWidth返回的是Drawable的原始宽高.
  • 直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content时就相当于使用match_parent

    解决方法: 在wrap_content时,给View设置一个默认的内部宽/高

//解决示例
    @Override
    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(200, 200);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(200, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, 200);
        }
    }

ViewGroup的measure过程

  • ViewGroup是一个抽象类,没有重写onMeasure,提供了一个measurechild的方法(先拿到子View的LayoutParams,根据LayoutParams确定其MeasureSpec,接着将MeasureSpec传递给子view进行测量)
    因为不同的ViewGroup的布局特性不一样,导致其测量细节各不相同.
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);
    }

在某些极端情况下,系统可能需要多次measure才能确定view的大小.
比较好的习惯是在onLayout获取view的宽高

四种方法获取View的宽高

  • 通过如下四种方法获取View的宽高

    1. Activity/View#onWindowFocusChanged
    2. view.post(runnable)runnable将消息投递到队列的尾部
    3. ViewTreeObserver
    4. 手动调用View#measure方法;
  • 第四种方式需要根据LayoutParams分情况处理

//1. MATCH_PARENT: 无法测量
//2. 具体值 dp/px
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,EXACTLY);
//3. wrap_content
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,AT_MOST);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,AT_MOST);//理论上能支持的最大值来构造
view.measure(widthMeasureSpec,heightMeasureSpec);

二. layout 过程

layout用于ViewGroup确定子元素的位置.

  1. layout方法确定view本身的位置,但onLayout确定所有子元素的位置.
  2. layout中首先通过setFrame来确定l,r,t,b四个位置,接着调用onLayout.
  3. View 的onLayout是空方法,ViewGroup的onLayout是一个抽象方法,不同ViewGroup的布局特性不一致.
  4. 自定义ViewGroup中的onLayout中会遍历调用子view的layout过程
  5. getMeasuredWidth和getWidth只是赋值时机不同(测量宽高的赋值时机要稍微早一些),值一般相等
//在自定义view中将r增大,则导致`最终宽高`和`测量宽高`不一致
 @Override public void layout(int l, int t, int r, int b) {
    super.layout(l, t, r+100, b);
  }

三. draw 过程

background.draw (绘制背景)-> 
onDraw (绘制自己)->
dispatchDraw(draw child)(绘制children) -> 
onDrawScrollBars.(绘制装饰)
  1. dispatchDraw用于绘制传递
  2. onDraw一般是一个空方法,不同的子View/子ViewGroup绘制过程是不同的.
  3. View的dispatchDraw是空方法,ViewGroup的dispatchDraw有代码.
  4. setWillNotDraw:如果一个View不需要绘制任何内容,设置这个标记位true后,系统会进行优化.
  5. 默认情况下View没有开启,ViewGroup开启了setWillNotDraw.
  6. 当明确知道一个ViewGroup需要通过onDraw来绘制内容时,需要显示地关闭WILL_NOT_DRAW标记.

自定义View

  1. 继承View,实现onDraw,需要支持wrap_content和padding处理
  2. 继承ViewGroup,需要处理自己和子元素的测量和布局过程.
  3. 继承特定的View(TextView),不需要处理wrap_content和padding
  4. 继承特定ViewGroup(LinearLayout),和2差不多

需要注意

  1. 支持wrap_content,否则wrap_content就和match_parent效果相同.
  2. 处理好padding,(需要在draw中处理padding)否则padding属性是无法起作用的
  3. 直接继承自ViewGroup 的控件需要在onMeasureonLayout中处理 paddingmargin.
  4. 不需要使用handler,因为View有post方法
  5. 线程和动画及时停止View#onDetachedFromWindow,否则会导致内存泄漏
  6. 嵌套滑动,需要冲突处理,参考第3章

延伸阅读:
Android视图绘制流程完全解析,带你一步步深入了解View(二)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章