Android View Analysis

Android View 绘制流程一般分为三个过程:measure、layout、draw

一、mesure

        android整个ui界面,本质上是多个View的树,其中ViewGroup是View的子类,他可以包含着多个子View和ViewGroup(ViewGroup之间也可以嵌套),这样形成了一个View树(如下图)。而measure函数的作用是为整个View树计算实际的大小,设置每个View对象的布局(窗口)大小。实际对应属性就是View类中的mMeasuredHeight(高)和mMeasuredWidth(宽)。

        在View类中measure过程主要涉及一下三个函数:

public final void measure(int widthMeasureSpec, int heightMeasureSpec)
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight)
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

       前两个函数都是final类,所以不能重载,为此在ViewGroup派生的非抽象类中我们必须重载onMeasure函数。实现measure的原理是:假如View还有子View,则measure子View,知道所有的子View都measure完后,再measure自己。ViewGroup中的measureChild或measureChildWithMargins就是实现这个功能的。

      在具体介绍测量原理之前,我们还是先了解一些基础知识。即measure函数的参数是由MeasureSpec类的makeMeasureSpec函数方法生成的一个32位整数,该整数的高2位表示模式(mode),低30位则是具体的尺寸大小(specSize)。

      MeasureSpec有三种模式(mode),分别是:UNSPCIFIED、EXACTLY和AT_MOST,各表示的意义如下:

模式(mode)含义
UNSPCIFIED对于空间的尺寸来说,没有任何参考意义
EXACTLYspecSize代表的是精确尺寸
AT_MOSTspecSize代表的是最大可获取的尺寸

      那么对于一个View的上述mode和specSize值是如何获取到的呢,他们是根据View的LayoutParams:

         ①当参数为fill_parent或者match_parent时,Mode为EXACTLY,specSize为剩余的所有空间。

         ②当参数为具体数值,譬如像素值(px或dp),Mode为EXACTLY,specSize为传入的数值。

         ③当参数为wrap_content时,Mode为AT_MOST,specSize运行时决定。

      上面提供的Mode和specSize仅仅是程序员对View的一个期望尺寸,最终一个View对象能从父视图那的到多大的允许尺寸则由子视图的期望尺寸和父视图的能力尺寸可提供尺寸)两方面决定。关于期望尺寸的设定,可以通过在布局资源文件中定义的android:layout_width和android:layout_height来设定,也可以通过代码在addView函数调用时传入LayoutParams参数来设定。父View的能力尺寸归根到底就是DecorView,这个尺寸是全屏的,由手机的分辨率来决定。期望尺寸能力尺寸和最终允许尺寸的关系,我们可以通过阅读measureChild或measureChildWithMargins都会调用的getChildMeasureSpec函数的源码来获得,下面简单说明三者之间的关系:

父视图能力尺寸子视图期望尺寸子视图最终允许尺寸
EXACTLY+size1EXACTLY+size2EXACTLY+size2
EXACTLY+size1fill_parent/match_parentEXACTLY+size1
EXACTLY+size1wrap_contentAT_MOST+size1
AT_MOST+size1EXACTLY+size2EXACTLY+size2
AT_MOST+size1fill_parent/match_parentAT_MOST+size1
AT_MOST+size1wrap_contentAT_MOST+size1
UNSPCIFIED+size1EXACTLY+size2EXACTLY+size2
UNSPCIFIED+size1fill_parent/match_parentUNSPCIFIED+0
UNSPCIFIED+size1wrap_contentUNSPCIFIED+0

      上述表格是子视图最终得到的允许尺寸,显然1、4、7项没有对size1和size2进行比较,所以允许尺寸是可以大于父视图的能力尺寸的,这个时候最终的视图尺寸应该是多少呢?AT_MOST和UNSPECIFIED的View又该如何决策最终的尺寸呢?

      通过Demo演示得到的结果,如果size2比size1大,在不使用滚动条的情况下,子视图超出部分将会被裁减掉,该父亲视图中如果在该子视图后面还有其他视图,那么也将会被裁减掉,但是通过调用其getVisibility函数,还是显示该控件是可见的,所以裁减后的控件依然是有的,只是用户没法看到;在使用滚动效果的情况下,就能将原本被裁减掉的控件通过滚动显示出来。

      对于第二个问题,根据源码View的onMeasure函数调用的getDefaultSize函数获知,在默认情况下,控件都有一个最小尺寸,该值可以通过android:minHeight和android:minWidth来设置(无设置缺省为0),在设置了背景的情况下,背景drawable的最小尺寸与前面设置的最小尺寸进行比较,取大值,作为控件的最小尺寸。在UNSPCIFIED情况下,就选用这个最小尺寸,其他情况则根据允许尺寸来。不过这个是默认规则,通过Demo发现,TextView在AT_MOST+size的情况下,并不是以size作为控件的最终尺寸,结果在发现的TextView源码中,重载了onMeasure函数,有价值的代码如下:

……
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
……
if (widthMode == MeasureSpec.AT_MOST) {
    width = Math.min(widthSize, width);
}
……
if (heightMode == MeasureSpec.AT_MOST) {
    height = Math.min(desired, heightSize);
}
……
      至于其中的width和desire值,可以在关注一下,虽然FrameWork提供了视图尺寸的默认计算规则,但是最的视图布局大小可以通过从在OnMeasure函数来修改计算规则,当然也可以不计算通过setMeasuredDimension来设置,需要注意的是:如果通过setMeasuredDimension的同时还要调用父类的onMeasure函数,那么在调用父类函数之前调用的setMeasuredDimension会无效果。

 二、Layout

      上述measure过程达到的结果是设定了视图的宽和高,layout过程的作用就是设定视图在父视图中的四个点(分别对应View的四个成员变量,mLeft、mTop、mRight、mBottom) 。同样layout也是被final修饰符限定的不能被重载,不过在ViewGroup中onLayout函数被abstract修饰,即所有派生自ViewGroup的类必须实现onLayout函数,从而实现对其包含的所有的子视图的布局设定。

      那么上述的measure结果与layout有什么关系呢,截取ViewRoot和FrameLayout两个类中的部分代码如下:

//ViewRoot的performTraversals函数measure之后对layout的调用代码
host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);
//FrameLayou的onLayout函数部分源码
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        final int count = getChildCount();
        ……
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();
                int childLeft = parentLeft;
                int childTop = parentTop;
                final int gravity = lp.gravity;
 
                if (gravity != -1) {
                    final int horizontalGravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
                    final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
 
                    switch (horizontalGravity) {
                        case Gravity.LEFT:
                            childLeft = parentLeft + lp.leftMargin;
                            break;
                        case Gravity.CENTER_HORIZONTAL:
                            childLeft = parentLeft + (parentRight - parentLeft - width) / 2 + lp.leftMargin - lp.rightMargin;
                            break;
                        case Gravity.RIGHT:
                            childLeft = parentRight - width - lp.rightMargin;
                            break;
                        default:
                            childLeft = parentLeft + lp.leftMargin;
                    }
 
                    switch (verticalGravity) {
                        case Gravity.TOP:
                            childTop = parentTop + lp.topMargin;
                            break;
                        case Gravity.CENTER_VERTICAL:
                            childTop = parentTop + (parentBottom - parentTop - height) / 2 + lp.topMargin - lp.bottomMargin;
                            break;
                        case Gravity.BOTTOM:
                            childTop = parentBottom - height - lp.bottomMargin;
                            break;
                        default:
                            childTop = parentTop + lp.topMargin;
                    }
                }
 
                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }
      从代码显示,具体layout布局时,就是根据measure过程设置的高和宽,结合视图在父视图中的起始位置,再外加视图的layoutgravity属性来设置四个点的具体位置(在LinearLayout中还会增加对layoutweight属性的考虑)。这个过程相对没有measure那么复杂。
      需要注意的是,在自定义组合控件时,可以根据需要不用或只用部分measure过程计算得到尺寸,具体可以看下之前做的下拉控件直接重载onLayout函数:
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    if (getChildCount() > 2) {
        throw new IllegalStateException("NPullToFreshContainer can host only two direct child");
    }
        
    View headView = getChildAt(0);
    View contentView = getChildAt(1);
    if(headView != null){
     headView.layout(0, -HEAD_VIEW_HEIGHT + mTatolScroll, getMeasuredWidth(), mTatolScroll);// mTatolScroll是下拉的位移值
    }
   
    if(contentView != null){
    contentView.layout(0, mTatolScroll, getMeasuredWidth(), getMeasuredHeight());
    }
        
    if (mFirstLayout) {        
     HEAD_VIEW_HEIGHT = getChildAt(0).getMeasuredHeight();
       mFirstLayout = false;
    }
}
三、Draw
      View的draw过程,相对来说比measure更为复杂,正因其复杂,多以android框架层已经将draw考虑的相当周全,虽然View类的draw函数没有用final修饰,但是我们自定义View,一般也不需要去重载实现它,这个过程先不说了,做个标注,看代码吧:
         draw()方法实现的功能流程如下:
              1、调用background.draw(canvas)绘制该View的背景
              2、调用onDraw(canvas)方法绘制视图本身(每个View都需要重载该方法,ViewGroup不需要实现该方法)
              3、调用dispatchDraw(canvas)方法绘制子视图(ViewGroup类已经为我们重写了dispatchDraw ()的功能实现,其内部会遍历每个子视图,调用drawChild()去重新回调每个子视图的draw()方法)
              4、调用onDrawScrollBars(canvas)绘制滚动条
      为了说明measure、layout和draw过程的连续性,摘得draw中的源码如下:
if (mBackgroundSizeChanged) {
    background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);
    mBackgroundSizeChanged = false;
}
      上述的mLeft,mTop,mLeft,mBottom就是我们在layout是设定的结果值,这里之所以要用减法获取高宽尺寸而不用measure过程设定的mMeasuredHeight和mMeasureWidth,个人感觉就是因为我们可以在代码中通过直接调用View的layout函数避开measure测算结果而导致真实高宽不等于mMeasuredHeight和mMeasureWidth这种情况。
      上述代码中的mBackgroundSizeChanged是个私有成员变量,源码中只能在View的onScrollChanged(int l, int t, int oldl, int oldt) 、layout过程调用的setFrame(int left, int top, int right, int bottom) 和setBackgroundDrawable(Drawable d)这三个函数中对其修改为true。
      到这里,除了具体的绘制外,我们对从Activity到View的绘制流程应该比较清楚了。

 

 

 

 

 

 

      

 

  

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值