自定义View基础之Layout

当上一节执行完measure操作后,接下来的layout过程其实是比较简单的,其目的就是父视图按照
子视图的大小及布局参数,将子视图放置到合适的位置上。布局参数最核心的是处理gravity参数。

layout过程的设计思路

最初调用 layout()函数是从 ViewRoot 类的 performTraversals(),host 是一个 View 对象,View 类中 layout()函数的类型是final,其子类不能重载该函数,以保证View系统中layout过程不变。

layout()函数中,首先调用setFrame()函数给当前视图设置参数中指定的位置,然后回调onLayou()函数。ViewGroup类中重载了onLayout()函数,并且将其函数类型设置成了一个abstract类型,因此, 所有的ViewGroup实例必须实现onLayout()。View系统希望程序员能够在onLayout()函数中对该视图所包含的子视图进行layout操作,当该视图是ViewGroup时,程序员一般会在onLayout()函数中调用子视 图 child的 layout()方法,从而完成对子视图的位置分配。当然,如果子视图也是一个ViewGroup实例, 就又会调用相应的onLayout()函数。

View类 中 layout()函数的原型为:

public void layout(int l, int t, int r, int b) 

参数指定了该子视图在父视图中的左、上、右、下的位置。该函数的
内部流程如下。

  1. 调用setFrame(l, t, r, b)将位置参数保存起来。

这些参数将保存到View内部变量(mLeft、mTop、 mRight、mBottom),保存完变量前,会先对比这些参数是否和原来的相同,如果相同,则什么都不做, 如果不同才进行重新赋值,并且在赋值前,会给mPrivateFlags中添加DRAWN标识,同时调用invalidate告诉View系统原来占用的位置需要重绘。注意是先调用invalidate(),而后赋值的,因为需要重绘的是老的区域。

  1. 回调onLayout()。

View中定义的onLayout()函数默认什么都不做,View系统提供onLayout()函数的目的是为了使系统包含有子视图的父视图能够在onLayout()函数对子视图进行位置分配,正因为如此,如果是父视图,就必须重载onLayout(),也正因为如此,ViewGroup类才把onLayout()重载修改为了一个abstract 类型。

  1. 清除 mPrivateFlags中的LAYOUT_REQUIRED标识,因为layout的操作已经完成了。
LinearLayout 中 onLayout()内部过程

LinearLayout中的子视图有两种排列方式,一种是水平的,另一种是垂直的,本节仅说明垂直方式
下的工作流程。
onLayout()函 数 中 首 先 根 据 mOrientation变 量 判 断 是 水 平 还 是 垂 直 ,如 果 是 垂 直 ,则调用layoutVertical()开始进行 layout 操作。

1. 获得子视图可用的宽度。

这里是进行垂直方向上的布局,为什么却要可用的宽度,而不是可用的髙度呢?

因为,就算是垂直方向,子视图本身也货以水平居中,而要居中就得知道可用的宽度是多少,从而计算出子视图左边沿的位置,如 图 所示,至于高度信息将在后面步骤中获得。
在这里插入图片描述
2. 根据父视图中的gravity属性,决定子视图的起始位置。
在默认情况下,gravity是 TOP,应用 程序可以设置为BOTTOM或 者CENTER_VERTICAL,这三者对应的起始位置如图所示。
在这里插入图片描述

3. 此时已经确定了子视图的垂直方向上的位置,于是就开始for()循环为每一个子视图分配位置。

(1) 取出子视图的LayoutParams,并得到gravity的值。注意这个gravity值并不是子视图中
android:gravity的值,而是android:layou_gravity的值,关于这一点的解释见下一小节。

( 2 ) 根 据 gravity的值确定水平方向上的起始位置,分为三种,分 别 是 LEFT、 CENTER HORIZONTAL及 RIGHT,三者对应的起始位置如图所示。

在这里插入图片描述

( 3 ) 调 用 setChildFrame(),该函数内部实际上就是调用child.layout()为子视图设置布局位置。

至此,LinearLayout就完成了为所包含的子视图分配布局位置的过程。

TextView 中 gravity 与 layout 的关系

上一节说到,子视图中gravity的值并不是android:gravity的值,而 是 android:layout__gravity,这是
怎么回事呢?
这个问题其实包含了一个重要概念,那就是 LayoutParams是如何被赋值的。应用程序可以在layout.xml文件中使用诸如< LinearLayout >、< TextView > 标签定义一个界面。无论是ViewGroup还是View,这些标签定义的仅仅是一个视图,而不能定义LayoutParams,因为LayoutParams正如其名称所讲,它是一个布局属性,而一个视图在没有被布局时又哪里来的布局属性呢?

而 在 过 去 的 开 发 经 验 中 ,大 家 几 乎 都 使 用 过 android:layout_height、android:layout_width、 android:gravity等 XM L语法为一个LinearLayout或 者 TextView添加相应的布局属性,并且这些值似乎都工作正常,表面上看,它就是该视图对应的LayoutParams值,这又作何解释?

另外,在LayoutInflater类中有一个inflate(int resId,View root)函数,在大多数情况下,大家调用该
函数时,第二个参数root都为空。那么这个参数的意义又是什么?为什么大多数情况下都要为空呢?

要回答以上问题,就需要弄清楚系统内部是如何从一个XM L文件创建一个对应的View树对象的,
以下面这段XML代码为例说明该过程:

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <TextView
            android:layout_width="200dp"
            android:layout_height="60dp"
            android:background="@color/white"
            android:text="am i center_vertical"
            android:gravity="center_horizontal"/>
        <ListView
            android:layout_width="300dp"
            android:layout_height="100dp"
            android:background="@color/black"/>
    </LinearLayout>

该段代码包含了一个LinearLayout,其中又包含两个子视图,分别是一个TextView及一个ListView。

从 XML文件中构造对应的View树是在Layoutlnflater中 的 inflate()函数中完成的,如果该段代码是作为setContentView()的参数,则会被当作Activity的界面,那么LinearLayout中定义的layout_width及height都是有意义的。但如果这段代码仅仅是应用程序调用inflate()函数创建一个View对象,那 么 layout_width及 height就没有任何意义。

inflate()函数会首先判断参数root是否为空,如果为空,就以XML文件中的根视图构造一个临时
的 View对象,此处是LinearLayout对象,此时该LinearLayout中声明的layout_width及 height都没有任何用处。接下来继续读取XML文件中TextView,读取TextView后,就需要把该TextView添加到 LinearLayout中。因为要添加,所以必须有一个LayoutParams对象,于是,inflate()函数中就以TextView中定义的layout_width及 height为参数,回 调 TextView父视图generateLayoutParams()函数。
TextView 的父视图正是 LinearLayout,而在 LinearLayout 的 generateLayoutParams(attrs)函数中,参数 attrs正是来源于TextView中所声明的全部属性值,除了 layout_width和 height夕卜,还包含background、text等所有声明的属性值,但 generateLayoutParams(atts)中却只对其中四个属性感兴趣:

public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
            TypedArray a =
                    c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout_Layout);

            weight = a.getFloat(com.android.internal.R.styleable.LinearLayout_Layout_layout_weight, 0);
            gravity = a.getInt(com.android.internal.R.styleable.LinearLayout_Layout_layout_gravity, -1);

            a.recycle();
        }

在以上代码中,super(c,attrs)中将读取layout_heightlayout_width的属性,如以下代码所示。然后继续读取weight属性和gravity属性,而这两个属性的名称却分别是layout_weightlayout_ gravity
generateLayoutParams(atts)最终将根据 TextView中的以上四个属性构造一个 LinearLayout.ayoutParams 对象,并以该对象把 TextView 添加到 LinearLayout 中。

回过头来再看上面提出的问题,< TextView >标签中包含的android:layout_width、height等并没有定义LayoutParams对象,它只是提供了构造LayoutParams参数所需要的值,而真正构造LayoutParams对象的是它的父视图,这些参数最终会产生什么样的LayoutParams取决于该TextView的父视图中generateLayoutParams(attrs)的具体实现。

这也就是上一节中所说的为什么要使用layout_gravity而不是gravity的原因,因为在LinearLayout
generateLayoutParams()函数中,是把 android:layout_gravity 属性值赋值给了 LayoutParams.gravity,而不是 android:gravity。事实上,android:gravity属性将作为TextView内部的参数,TextView在内部的 onDraw()函数中将根据android:gravity的值决定把文字显示在什么地方。

而当调用setContentView(int resld)时,系统内部也同样调用了 inflate()方法从指定的XML文件中产生 View树,并且调用时参数root设置的是内部的一个FrameLayout对象。root参数的意义在于它将提 供generateLayoutParams()函数,该函数产生的LayoutParams对象将作为XML文件中包含的视图被添加到 root时的布局属性。

如果你在一个XM L文件中仅仅定义了一个TextView标签,然后调用inflate(xml, null)从这个XML
文件中infalte出一个TextView对象,那么该TextView标签中所包含的android:layout_width、height将没有任何意义。当 你 想 把 infalte出 来 的 TextView对象添加到某个ViewGroup时,还必须构造一个 LayoutParams 对象。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值