Android的View工作原理(一)mearsure过程

       在界面上Android提供了一套GUI库,里面有很多控件,但是很多情况我们并不满足于系统提供的控件,因为这样就意味着应用界面的同类化比较严重。怎么样才能做到与众不同呢?答案就是自定义View,也就是自定义View,也可以叫自定义控件。但是要实现一个自定义View是有一定难度的,大部分时候我们仅仅了解基本控件的使用方法是无法做出复杂的自定义View的。为了更好的自定义View,我觉得有必要掌握View的底层工作原理,比如View的测量、布局和绘制流程,这样就可以做出一个比较完善的自定义View。另外只有对View的足够了解,才能选择出最适合当前需要的自定义View的实现方式。

       本篇博文主要介绍View的mearsure、layout和draw三大流程中的mearsure,后面两个会继续在博客中更新,欢迎访问。下面先介绍两个基本概念:ViewRoot和DecorView。ViewRoot的实现是ViewRootImpl,他是连接WindowMangger和DecorView的纽带,WindowManager这在以前的博客已经详细介绍过了。DecorView是最顶级的View,一般情况下他的内部会包含一个竖直方向的LinearLayout,在这个LinearLayout里面有上下两部分,上面是标题栏,下面是内容栏。那在Activity中我们通过setContentView所设置的布局文件其实就是被加载在内容栏中的。内容栏在系统中的id就是content(android.R.id.content),所以这里叫setContentView可能有这个原因吧。DecorView其实是一个FrameLayout,View层的事件都要先经过DecorView,然后才传递给我们的View。

       除了ViewRoot和DecorView,我们还需要了解的一个概念是MearsureSpec,翻译过来大概就是测量细则。在测量过程中,系统会将View的LayoutParams根据父容器所施加的约束转换为对应的MeasureSpec(由LayoutParams和父容器共同决定),继而按照这个MearsureSpec就可以在onMearsure中确定View的测量宽/高。不同的是,对于顶级View DecorView而言,MearsureSpec由窗口尺寸和自身LayoutParams决定。记住这里只是测量的宽和高,并不一定等于最终的宽和高。

MeasureSpec代表一个32位int值,高2位代表SpecMode,低30位代表SpecSize,前者指测量模式,后者就是某种测量模式所对应的View大小。之所以将两者打包成int值其实避免了过多的对象内存分配。

SpecMode有以下三种,他和LayoutParams的对应关系在下面会介绍:

·UNSPECIFIED:父容器不对View有任何限制,要多大给多大

·EXACTLY:父容器已经检测出View所需要的大小,对应于LayoutParams中的match_parent和指定具体数值两种情况。

·AT_MOST:父容器指定了一个可用的大小,View大小不能超过这个值。对应于LayoutParams中的wrap_content。

       对于我们布局中的View来说,mearsure过程由ViewGroup传递而来。在ViewGroup的mearsureChildWithMargins方法中先通过getChildMearsureSpec方法得到子元素的MearsureSpec,接着调用子元素的mearsure方法。要注意子元素MearsureSpec的创建与父容器的MearsureSpec、子元素本身的LayoutParams、View的margin及padding都有关,padding是指父容器中已经占用的空间大小。

LayoutParams的宽高有以下三种模式:

·LayoutParams.MATCH_PARENT:精确模式(EXACTLY),大小就是窗口大小

·LayoutParams.WRAP_CONTENT:最大模式(AT_MOST),大小不能超过窗口大小

·固定大小(像素):精确模式(EXACTLY),大小就是LayoutParams指定大小

结合LayoutParams需要注意以下三点:

       当View指定了固定大小,View的大小遵循LayoutParams中的大小,与父容器的MearsureSpec无关。

       当View宽高都是wrap_content的时候,View的模式总是最大化,但不能超过父容器大小。

       当View宽高都是match_parent的时候,如果父容器是精准模式,那么View也是精准模式并且大小是父容器剩余空间;如果父容器是最大模式,那View也是最大模式并且大小不会超过父容器的剩余空间。

       在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。经过mearsure、layout和draw三个过程才能最终将一个View绘制出来,其中Mearsure得到View测量的宽和高,Layou确定了View四个定点的坐标和实际View的宽和高(也就是View在父容器中的位置),Draw决定了View的显示。

       整个过程是从ViewRoot的performTraversals开始的,流程图如下:

       可以看出performTraversals会依次调用performMearsure、performLayout和performDraw三个方法,这三个方法分别完成顶级View的mearsure、layout和draw的过程,performMearsure又会调用mearsure方法,mearsure方法继而调用onMearsure方法,在onMearsure方法中又调用了mearsure方法,如果只是一个原始View那么通过mearsure方法就完成了测量过程,如果是一个ViewGroup,除了完成自己的测量外还会去调用其他子元素的mearsure方法,各个元素再通过onMeasure递归去执行这个流程。其他两个类似。

       在ViewGroup中有onMearsure方法,在View中同样有这个方法。View的mearsure会去调用View的onMearsure方法,接着又调用了他的getDefaultSize方法,这个方法中View的宽高由MearsureSpec的specSize决定,也就可以得出一条结论:直接继承View的自定义控件需要重写onMearsure方法并设置wrap_content时自身的大小,否则使用wrap_content时相当于使用march_parent。为什么这么说,需要结合上面的SpecMode解释,当View使用wrap_content时他的SpecMode也就是最大模式,这种情况下View的specSize是父容器中目前剩余的大小,很显然View的宽高等于父容器当前剩余的大小,效果和match_parent一致。那么解决方法就是在重写的onMearsure方法中给wrap_content模式的View设置默认宽高就行了,具体的代码实现如下:

Protected void onMearsure(int widthMearsureSpec,intheightMearsureSpec){

   super.onMearsure();

   int widthSpecMode =MearsureSpec.getMode(widthMearsureSpec);

   int widthSpecSize =MearsureSpec.getSize(widthMearsureSpec);

   int heightSpecMode =MearsureSpec.getMode(heightMearsureSpec);

   int heightSpecSize =MearsureSpec.getSize(heightMearsureSpec);

   if(widthSpecMode ==MearsureSpec.AT_MOST

&& heightSpecMode == MearsureSpec.AT_MOST){

           setMearsuredDimension(mWidth,mHeight);

}else if(widthSpecMode ==MearsureSpec.At_MOST){

    setMearsuredDimension(mWidth,heightSpecSize);

}else if(heightSpecMode ==MearsureSpec.At_MOST){

    setMearsuredDimension(widthSpecSize,mHeight);

}

}

       下面就讲讲ViewGroup是怎样遍历调用子元素的mearsure方法ViewGroup是一个抽象类,他没有重写View的omMearsure方法,但是他提供了一个叫mearsureChildren的方法。然后通过mearsureChild取出子元素的LayoutParams,创建子元素的MearsureSpec,接着将得到的MearsureSpec直接传递给View的mearsure方法进行测量。从上面的流程图可以看出ViewGroup也有一个onMearsure方法,但是这个方法中并没有定义他的具体测量过程,这是因为ViewGroup的不同子类,比如LinearLayout、RelativeLayout等,他们有不同的布局特性,测量细节各不相同,ViewGroup无法在onMearsure方法中做统一实现,所以他的onMearsure方法就交给了不同的子类去具体实现。

       下面以竖直布局的LinearLayout如何实现自身的测量。先查看他的mearsureVertical方法,其中关键的一句是:

mearsureChildBeforeLayout(child,i,widthMearsureSpec,0,heightMearsureSpec,totalWeight ==0?mTotalLength:0);

       可见系统会遍历子元素,并对子元素执行mearsureChildBeforeLayout方法,这个方法在内部会调用mearsure方法,这样各个元素就开始进入绘制过程,并且系统会通过mTotalLength这个变量存储LinearLayout在竖直方向上的初步高度。每测量一个子元素,mTotalLength就会增加,增加的部分主要包括子元素的高度以及子元素在竖直方向上的margin等。当子元素测量完毕后,LinearLayout还会测量自己的大小。LinearLayout测量自己大小分为竖直方向和水平方向两个过程。针对竖直方向的LinearLayout而言,他在水平方向上的测量遵循前面讲的View的测量过程,他在竖直方向上的测量有如下几个情况:

·如果布局中高度采用match_parent或者具体值(精确模式),测量过程和View一致,即高度为是specSize。

·如果布局中采用wrap_content,他的高度是所有子元素占用高度的总和,还要考虑竖直方向上的padding,但不能超过父元素的剩余空间。这在编程中是一个常识。

       Mearsure是View三大流程中最复杂的一个,mearsure结束后就可以通过getMearsureWidth/Height获得View的测量宽/高。但是某些极端情况下需要多次mearsure才能确定最终的大小,这种情况下拿到的测量宽高是不准确的。如果我们在Activity已启动时需要获取某个View的宽高,第一感觉可能是在onCreate或者onResume中获取,但是要知道View的mearsure过程和Activity的生命周期不是同步执行的,如果Activity执行了onCreate或者onResume是某个View还没有测量完毕,那么获得的宽高就是0。

可以通过下面4中方法解决这个问题:

(1)   onWindowFocusChanged

这个方法的含义是View已经初始化完毕了。这个方法在Activity的窗口得到焦点和失去焦点时均会被调用一次。也就是说当Activity继续执行和暂停执行时(调用onResume和onPause)他都会被调用。

public void onWindowFocusChanged(booleanhasFocus){

   super. onWindowFocusChanged(hasFocus);

   if(hasFocus){

       int width =view.getMearsureWidth();

       int height =view.getMearsureHeight();

}

}

(2)   view.post(runnable)

通过post将一个任务投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View已经初始化完毕了。

protected void onStart(){

   super.onStart();

   view.post(newRunnable(){

       @Override

       public voidrun(){

           int width= view.getMearsureWidth();

       int height =view.getMearsureHeight();

}

});

}

(3)   ViewTreeObserver

使用该类的众多回调可以完成,比如使用OnGlobalLayoutListener这个借口,。当View树的状态发生改变或者View树内部的View可见性发生变化时,onGlobalLayout将会被回调,因此这是一个获取View大小的好时机。

protected void onStart(){

   super.onStart();

 ViewTreeObserve observe =view.get ViewTreeObserve();

 observe.addOnGlobalLayoutLiatener(newOnGlobalLayoutListener(){

     @SuppressWarning(“deprecation”)

     @Override

     public voidonGlobalLayout(){

         view.getViewTreeObserver().removeGlobalOnLayooutListener(this);

           int width = view.getMearsureWidth();

           int height= view.getMearsureHeight();

})

}

 

(4)   View.mearsure(intwidthMearsureSpec,int heightMearsureSPec)

手动对View进行mearsure来得到宽高,这需要根据View的LayoutParams分情况处理:

·具体指定大小

比如宽高都是100px

int widthMearsureSpec = Mearsure.makeMearsureSpec(100,MearsureSpec.EXACTLY);

int heightMearsureSpec = Mearsure.makeMearsureSpec(100,MearsureSpec.EXACTLY);

view.mearsure(widthMearsureSpec, heightMearsureSpec);

 

·match_parent

无法得到。因为根据View的Mearsure过程,构造此种MearsureSpec需要知道parentSize,即父容器的剩余空间,但这个时候我们无法知道父容器的剩余空间,所以理论上无法得到。

 

·wrap_content

如下所示:

int widthMearsureSpec = Mearsure.makeMearsureSpec((1<<30)-1,MearsureSpec.AT_MOST);

int heightMearsureSpec = Mearsure.makeMearsureSpec((1<<30)-1,MearsureSpec.AT_MOST);

view.mearsure(widthMearsureSpec, heightMearsureSpec);

注意这里有一个(1<<30)-1,前面见过View的大小是MearsureSpec后30位的二进制表示,最大是2^30-1,也就是(1<<30)-1。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值