美文网首页Android自定义View
Android View-布局原理及自定义onMeasure()

Android View-布局原理及自定义onMeasure()

作者: 高丕基 | 来源:发表于2018-05-01 21:01 被阅读690次

    这边主要说一些UI中布局相关的内容。

    1、布局过程

    1.1 含义

    程序在运行时利用布局文件的代码来计算出实际尺寸的过程。

    1.2 布局过程工作内容

    一共分两个阶段——测量和布局。过程如下流程图:

    测量阶段:

        从上到下递归调用每个View或者ViewGroup的measure()方法,测量他们的尺寸并计算它们的位置。在测量阶段,measure()方法被父View调用,在measure()中做一些准备和优化工作后,调用onMeasure()来进行实际的自我测量。onMeasure()做的事,View和ViewGroup不一样:

        View:View在onMeasure()中会计算出自己的尺寸然后保存;

        ViewGroup:ViewGroup在onMeasure()中会调用所有子View的measure()让它们进行自我测量,并根据子View计算出的期望尺寸来计算出它们的实际尺寸和位置然后保存。同时,它也会根据子View的尺寸和位置来计算出自己的尺寸然后保存;

    布局阶段:

    从上到下递归地调用每个View或者ViewGroup的layout()方法,把测得的它们的尺寸和位置赋值给它们。

    布局阶段,layout()方法被父View调用,在layout()中它会保存父View传进来的自己的位置和尺寸,并且调用onLayout()来进行实际的内部布局。onLayout()做的事,View和ViewGroup也不一样:

        View:由于没有子View,所以View的onLayout()什么也不做。

        ViewGroup:ViewGroup在onLayout()中会调用自己的所有子View的layout()方法,把它们的尺寸和位置传给它们,让它们完成自我的内部布局。

    2、布局过程自定义

    布局过程自定义主要在于重写view或viewGroup的onMeasure()和onLayout()分方法,一般分为三种情况:

    2.1 重写onMeasure()来修改已有的View的尺寸

    重写onMeasure()方法,并在里面调用super.onMeasure(),触发原有的自我测量。

    在super.onMeasure()的下面用getMeasuredWidth()和getMeasuredHeight()来获取到之前的测量结果,并使用自己的算法,根据测量结果计算出新的结果;

    调用setMeasuredDimension()来保存新的结果。

    下面是一个将imageview自定义成正方形imageview的例子:

    2 .2 重写onMeasure()来全新定制自定义View的尺寸

            全新定制自定义view尺寸和第一种情况有所区别,它没有super.onMeasure()方法可以调用。需要满足父view的限制和自己的需求来确定自己的尺寸。其中父view的限制即onMeasure(int widthMeasureSpec, int heightMeasureSpec)中的两个参数,这两个参数是在viewgroup中调用子view的measure(intwidthMeasureSpec, int heightMeasureSpec)时传进来的。子view在计算自己尺寸时候,需要满足这个限制。父view对子view的限制其实就是父view把开发者对子view的尺寸要求进行处理计算之后所得到的更精确的要求,所谓开发者的要求就是这个view在布局文件里面的layout_打头的属性。他们是用来设置view的位置和尺寸的,例如layout_width,layout_height,layout_gravity等等。这些layout_打头的属性不是给view看的,而是给它们的父view看的。也就是说在程序运行界面显示的时候,每一个viewGroup会读取它的子view的这些layout_打头的属性,然后用它们去进行处理和计算。例如开发者写layout_width= “match_parent”。要其填满父view的可用宽度,但此时具体可用宽度数值是多少,子view并不知道。此时父view就需要去计算父view自己的可用宽度是多少,例如是200像素,此时父view就会告诉子view其宽度是200像素。此时子view在onMeasure中必须将自己的宽度测算成200像素的宽度,再通过setMeasuredDimension(measureWidth,measureHeight)将200保存下来。

    总的来说,开发者对子view的尺寸要求在经过父view处理之后所得到的那个对子view的尺寸限制一共有三种:

    ① 不限制

    ② AT_MOST:限制上限

    ③ EXACTLY:限制固定值

    如果不遵守,程序会出奇怪的bug,即画面不会按照xml中布局的情况展示。

    子view该如何去迎合这一限制呢?调用resolveSize(measureWidth,widthMeasure)。如下图:

    resolveSize()源码简略版:

    2.3 重写onMeasure()和onLayout()来全新定制自定义ViewGroup的内部布局。

    只有ViewGroup需要重写onLayout()。

    2.3.1 重写onMeasure()来计算内部布局

    2.3.1.1 调用每个子view的measure(),让子view自我测量

    这一步是最关键的一步,核心是要计算childWidthMeasureSpec和childHeightMeasureSpec。这两个是父view对子view的尺寸限制,这两个尺寸限制,就是把开发者的要求,也就是这个子view在xml文件里面那些layout_打头的参数,把它们和这个ViewGroup它的剩余可用空间两者结合起来计算得到。其中,开发者的要求,优先级要高于可用空间。例如layout_width= “50dp” ,那就不用管内部有没有足够的空间给子view用,直接限制子view的尺寸是50dp,即mode是EXACTLY,而size是50dp所对应的像素值。如果开发者要求是具体的值直接用具体值作为size,mode为EXACTLY。如果开发者要求子view是填满父空间,mode为EXACTLY,size为此ViewGroup的可用空间。

    ViewGroup自己的可用空间如何获得?对于子view的测量,即调用子view的measure()方法,是发生在ViewGroup自己的onMeasure()中的,此时自己的尺寸并没有确定。此时只能得到ViewGroup的可用空间,即ViewGroup自己的onMeasure(int widthMeasureSpec, int heightMeasureSpec)中传入的参数。虽然这两个参数只是对ViewGroup的一份限制,不能直接决定自己的尺寸,但依据这份限制自己可以得到一个可用空间,即虽然我还没算出我自己多大,但是我知道我自己最多能有多大的地方去给自己和子view用。具体这个最多是多少要看ViewGroup的onMeasure()中传入的MeasureSpec参数中的mode:

    ① EXACTLY:

    可用空间是MeasureSpec中size。例如宽度的MeasureSpec中 mode是EXACTLY,size是500。相当于自己的可用宽度被提前决定为500,这样子view的可用宽度就是500像素。那么在分配第一个子view时候,就可以用500来作为可用宽度去测量(注意是第一个子view)。在测量之后,假设子view测得的宽度为200,那么在横向测量第二个子view时候,可用宽度就变成了300,以此类推。

    ② AT_MOST:

    它和EXACTLY的处理方式完全一样,因为虽然是一个上限,而不是固定值,是一个动态的未确定的范围,但测量子view时候,父view应该是在不超过这个最大值情况下随便用的原则,所以其可用空间为其尺寸上限,即MeasureSpec中size。

    EXACTLY和 AT_MOST只有在测量完子view,再测量自己的时候有所区别。区别如2.2节中所说。

    ③ UNSPECIFIED:

    对自己没有尺寸限制,也就是说自己的可用空间是无限的,所以它在计算子view的MeasureSpec就很简单,只要考虑子view的layout_参数即可。

    具体操作如下:

    2.3.1.2 根据子view给出的尺寸,得出子view的位置,并保存它们的位置和尺寸

    测量子view的过程其实是一个排版过程,故在测量过程中能拿到每一个子View的位置。然后保存子view位置就好。由于大多数情况view的测量尺寸就是最终尺寸,故要用时候调用它们的getMeasuredWidth()和getMeasuredHeight()即可。保存它们的目的是为它们在布局阶段onLayout()方法里面进行调用。在上图所示代码中可以在执行完childView.measure()以后根据实际需求情况保存子view的位置信息。

    注意:

    ① 并不是所有layout都需要保存子view位置。例如LinearLayout,其在LinearLayout的onLayout()方法中计算各子view的位置。

    ② 有些时候对某些子view需要重复测量才能正确得到尺寸和位置。(当一个纵向的LinearLayout宽度是wrap_content,而它测量的子view的宽度是match_parent时候,它会先直接用自己的MeasureSpec来测量这个子view,测量完后这个子view会得到它的宽度。但此时宽度并不是它的最终宽度。在所有子view都测量完成后,LinearLayout会把每一个宽度是match_parent的子view拎出来,重新单独测一遍。这次测量时候输入的mode是EXACTLY,size用的是其他子view里面宽度最宽的那个宽度。经过两次测量,其子view宽度才最终确定。)

    2.3.1.4 根据子view的位置和尺寸计算出自己的尺寸,并调用setMeasuredDimension(measureWidth,measureHeight)保存。

    根据需求决定gridView自己的尺寸大小。

    2.3.2 重写onLayout()来摆放子view

    这一步主要是调用每一个子view的layout()方法,把之前在onMeasure()里保存下来的它们的位置作为参数传进去,然它们进行自我布局。

    相关文章

      网友评论

        本文标题:Android View-布局原理及自定义onMeasure()

        本文链接:https://www.haomeiwen.com/subject/vdyfrftx.html