源码解析Android中View的layout布局过程

Android中的Veiw从内存中到呈现在UI界面上需要依次经历三个阶段:量算 -> 布局 -> 绘图,关于View的量算、布局、绘图的总体机制可参见博文 《 Android中View的布局及绘图机制》。量算是布局的基础,如果想了解量算的细节,可参见博文《源码解析Android中View的measure量算过程》。本文将从源码角度解析View的布局layout过程,本文会详细介绍View布局过程中的关键方法,并对源码加上了注释以进行说明。

对View进行布局的目的是计算出View的尺寸以及在其父控件中的位置,具体来说就是计算出View的四条边界分别到其父控件左边界、上边界的距离,即计算View的left、top、right、bottom的值。


layout

layout()方法是View布局的入口,其源码如下所示:

    public void layout(int l, int t, int r, int b) {
        //成员变量mPrivateFlags3中的一些比特位存储着和layout相关的信息
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            //如果在mPrivateFlags3的低位字节的第4位(从最右向左数第4位)的值为1,
            //那么就表示在layout布局前需要先对View进行量算,
            //这种情况下就会执行View的onMeasure方法对View进行量算
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            //量算完成后就会将mPrivateFlags3低位字节的第4位重置为0,
            //移除掉标签PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        //如果isLayoutModeOptical()返回true,那么就会执行setOpticalFrame()方法,
        //否则会执行setFrame()方法。并且setOpticalFrame()内部会调用setFrame(),
        //所以无论如何都会执行setFrame()方法。
        //setFrame()方法会将View新的left、top、right、bottom存储到View的成员变量中
        //并且返回一个boolean值,如果返回true表示View的位置或尺寸发生了变化,
        //否则表示未发生变化
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            //如果View的布局发生了变化,或者mPrivateFlags有需要LAYOUT的标签PFLAG_LAYOUT_REQUIRED,
            //那么就会执行以下代码
            //首先会触发onLayout方法的执行,View中默认的onLayout方法是个空方法
            //不过继承自ViewGroup的类都需要实现onLayout方法,从而在onLayout方法中依次循环子View,
            //并调用子View的layout方法
            onLayout(changed, l, t, r, b);
            //在执行完onLayout方法之后,从mPrivateFlags中移除标签PFLAG_LAYOUT_REQUIRED
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

            //我们可以通过View的addOnLayoutChangeListener(View.OnLayoutChangeListener listener)方法
            //向View中添加多个Layout发生变化的事件监听器
            //这些事件监听器都存储在mListenerInfo.mOnLayoutChangeListeners这个ArrayList中
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                //首先对mOnLayoutChangeListeners中的事件监听器进行拷贝
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    //遍历注册的事件监听器,依次调用其onLayoutChange方法,这样Layout事件监听器就得到了响应
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }

        //从mPrivateFlags中移除强制Layout的标签PFLAG_FORCE_LAYOUT
        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        //向mPrivateFlags3中加入Layout完成的标签PFLAG3_IS_LAID_OUT
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    }
  • 在layout()方法内部刚开始执行的时候,首先会根据mPrivateFlags3变量是否具有标志位PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT判断是否需要执行View的onMeasure()方法。如果具有标志位PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT,则执行onMeasure()方法,从而对View进行量算,量算的结果会保存到View的成员变量中。量算完成后就会将mPrivateFlags3低位字节的第4位重置为0,移除掉标签PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT。
  • 如果isLayoutModeOptical()返回true,那么就会执行setOpticalFrame()方法,否则会执行setFrame()方法。并且setOpticalFrame()内部会调用setFrame(),所以无论如何都会执行setFrame()方法。setFrame()方法会将View新的left、top、right、bottom存储到View的成员变量中,并且返回一个boolean值,如果返回true表示View的位置或尺寸发生了变化,否则表示未发生变化。后面会对setFrame()方法详细介绍。
  • 如果View的布局发生了变化,或者mPrivateFlags有需要LAYOUT的标签PFLAG_LAYOUT_REQUIRED,就会触发onLayout方法的执行,View中默认的onLayout方法是个空方法。不过继承自ViewGroup的类都需要实现onLayout方法,从而在onLayout方法中依次循环子View,并调用子View的layout方法。在执行完onLayout方法之后,从mPrivateFlags中移除标签PFLAG_LAYOUT_REQUIRED。然后会遍历注册的Layout Change事件监听器,依次调用其onLayoutChange方法,这样Layout事件监听器就得到了响应。
  • 最后,从mPrivateFlags中移除强制Layout的标签PFLAG_FORCE_LAYOUT,向mPrivateFlags3中加入Layout完成的标签PFLAG3_IS_LAID_OUT。

setFrame

setFrame()方法是具体用来完成给View分配尺寸以及位置工作的,在layout()方法中会调用setFrame()方法。其源码如下所示:

    protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;

        if (DBG) {
            Log.d("View", this + " View.setFrame(" + left + "," + top + ","
                    + right + "," + bottom + ")");
        }

        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            //将新旧left、right、top、bottom进行对比,只要不完全相对就说明View的布局发生了变化,
            //则将changed变量设置为true
            changed = true;

            //先保存一下mPrivateFlags中的PFLAG_DRAWN标签信息
            int drawn = mPrivateFlags & PFLAG_DRAWN;

            //分别计算View的新旧尺寸
            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            //比较View的新旧尺寸是否相同,如果尺寸发生了变化,那么sizeChanged的值为true
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

            // Invalidate our old position
            invalidate(sizeChanged);

            //将新的left、top、right、bottom存储到View的成员变量中
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            //mRenderNode.setLeftTopRightBottom()方法会调用RenderNode中原生方法的nSetLeftTopRightBottom()方法,
            //该方法会根据left、top、right、bottom更新用于渲染的显示列表
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

            //向mPrivateFlags中增加标签PFLAG_HAS_BOUNDS,表示当前View具有了明确的边界范围
            mPrivateFlags |= PFLAG_HAS_BOUNDS;

            if (sizeChanged) {
                //如果View的尺寸和之前相比发生了变化,那么就执行sizeChange()方法,
                //该方法中又会调用onSizeChanged()方法,并将View的新旧尺寸传递进去
                sizeChange(newWidth, newHeight, oldWidth, oldHeight);
            }

            if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
                //有可能在调用setFrame方法之前,invalidate方法就被调用了,
                //这会导致mPrivateFlags移除了PFLAG_DRAWN标签。
                //如果当前View处于可见状态就将mPrivateFlags强制添加PFLAG_DRAWN状态位,
                //这样会确保下面的invalidate()方法会执行到其父控件级别。
                mPrivateFlags |= PFLAG_DRAWN;
                invalidate(sizeChanged);
                //invalidateParentCaches()方法会移除其父控件的PFLAG_INVALIDATED标签,
                //这样其父控件就会重建用于渲染的显示列表
                invalidateParentCaches();
            }

            // 重新恢复mPrivateFlags中原有的PFLAG_DRAWN标签信息
            mPrivateFlags |= drawn;

            mBackgroundSizeChanged = true;
            if (mForegroundInfo != null) {
                mForegroundInfo.mBoundsChanged = true;
            }

            notifySubtreeAccessibilityStateChangedIfNeeded();
        }
        return changed;
    }
  • 在该方法中,会将新旧left、right、top、bottom进行对比,只要不完全相同就说明View的布局发生了变化,则将changed变量设置为true。然后比较View的新旧尺寸是否相同,如果尺寸发生了变化,并将其保存到变量sizeChanged中。如果尺寸发生了变化,那么sizeChanged的值为true。
  • 然后将新的left、top、right、bottom存储到View的成员变量中保存下来。并执行mRenderNode.setLeftTopRightBottom()方法会,其会调用RenderNode中原生方法的nSetLeftTopRightBottom()方法,该方法会根据left、top、right、bottom更新用于渲染的显示列表。
  • 如果View的尺寸和之前相比发生了变化,那么就执行sizeChange()方法,该方法中又会调用onSizeChanged()方法,并将View的新旧尺寸传递进去。
  • 如果View处于可见状态,那么会调用invalidate和invalidateParentCaches方法。invalidateParentCaches()方法会移除其父控件的PFLAG_INVALIDATED标签,这样其父控件就会重建用于渲染的显示列表。

sizeChange

sizeChange方法会在View的尺寸发生变化时调用,在setFrame()方法中就可能会调用sizeChange()方法。当然,在View的setLeft()、setTop()、setRight()、setBottom()等其他改变View尺寸的方法中也会调用sizeChange()方法,其源码如下所示:

    private void sizeChange(int newWidth, int newHeight, int oldWidth, int oldHeight) {
        //将View的新旧尺寸传递给onSizeChanged()方法
        onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);
        if (mOverlay != null) {
            mOverlay.getOverlayView().setRight(newWidth);
            mOverlay.getOverlayView().setBottom(newHeight);
        }
        rebuildOutline();
    }

在该方法中其主要将View的新旧尺寸传递给onSizeChanged()方法使其执行。


onSizeChanged

onSizeChanged()方法是个空方法,代码如下所示:

    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    }

该方法会在View的尺寸发生变化时,通过sizeChange()方法的执行而被调用。当View第一次加入到View树中时,该方法也会被调用,只不过传入的旧尺寸oldWidth和oldHeight都是0。


总结

layout方法总的调用过程主线如下所示:

layout() -> onMeasure() -> setFrame() -> sizeChange() -> onSizeChanged() -> onLayout() ->遍历执行OnLayoutChangeListener.onLayoutChange()

希望本文对大家理解View的layout布局过程有所帮助!

相关阅读:

《我的Android博文整理汇总》

《 Android中View的布局及绘图机制》

《源码解析Android中View的measure量算过程》

时间: 2024-10-02 07:24:08

源码解析Android中View的layout布局过程的相关文章

源码解析Android中View的measure量算过程

Android中的Veiw从内存中到呈现在UI界面上需要依次经历三个阶段:量算 -> 布局 -> 绘图,关于View的量算.布局.绘图的总体机制可参见博文< Android中View的布局及绘图机制>.量算是布局和绘图的基础,所以量算是很重要的一个环节.本文将从源码角度解析View的量算过程,这其中会涉及某些关键类以及关键方法. 对View进行量算的目的是让View的父控件知道View想要多大的尺寸. 量算过程概述 如果要进行量算的View是ViewGroup类型,那么ViewGr

深入源码解析Android中Loader、AsyncTaskLoader、CursorLoader、LoaderManager

如果对Loader.AsyncTaskLoader.CursorLoader.LoaderManager等概念不明白或不知道如何使用Loader机制,可参见博文Android中Loader及LoaderManager的使用(附源码下载).本文主要通过研究Loader及其子类的生命周期的方式来对Loader及其子类.LoaderManager的源码进行研究. Loader是靠LoaderManager管理的,LoaderManager可以同时管理多个Loader,即LoaderManager与Lo

深入源码解析Android中的Handler,Message,MessageQueue,Looper

本文主要是对Handler和消息循环的实现原理进行源码分析,如果不熟悉Handler可以参见博文< Android中Handler的使用>,里面对Android为何以引入Handler机制以及如何使用Handler做了讲解. 概括来说,Handler是Android中引入的一种让开发者参与处理线程中消息循环的机制.我们在使用Handler的时候与Message打交道最多,Message是Hanlder机制向开发人员暴露出来的相关类,可以通过Message类完成大部分操作Handler的功能.但

android源码解析(十八)--&gt;Activity布局绘制流程

这篇文章是承接上一篇文章(Android布局加载流程:http://blog.csdn.net/qq_23547831/article/details/51284556)来写的,大家都知道Activity在Android体系中扮演者一个界面展示的角色,通过上一篇文章的分析,我们知道Activity是通过Window来控制界面的展示的,一个Window对象就是一个窗口对象,而每个Activity中都有一个相应的Window对象,所以说一个Activity对象也就可以说是一个窗口对象,而Window

spring boot 源码解析52-actuate中MVCEndPoint解析

今天有个bie项目的jolokia的endpoint不能访问,调试源码发现:endpoint.enabled的开关导致的. 前言之前的几篇文章分析了spring boot 中有关endpoint的实现,细心的朋友可以发现,在org.springframework.boot.actuate.endpoint.mvc 包下也有一系列的xxxEndpoint,这又是为什么呢? 原因是: 我们很多情况下,都是访问接口的方式获取应用的监控,之前的分析是其实现的底层,要想实现通过接口访问,还需要对其进行包装

从源码看Android中sqlite是怎么读DB的

执行query 执行SQLiteDatabase类中query系列函数时,只会构造查询信息,不会执行查询. (query的源码追踪路径) 执行move(里面的fillwindow是真正打开文件句柄并分配内存的地方) 当执行Cursor的move系列函数时,第一次执行,会为查询结果集创建一块共享内存,即cursorwindow moveToPosition源码路径 fillWindow----真正耗时的地方 然后会执行sql语句,向共享内存中填入数据, fillWindow源码路径 在SQLite

从源码看Android中sqlite是怎么读DB的(转)

执行query 执行SQLiteDatabase类中query系列函数时,只会构造查询信息,不会执行查询. (query的源码追踪路径) 执行move(里面的fillwindow是真正打开文件句柄并分配内存的地方) 当执行Cursor的move系列函数时,第一次执行,会为查询结果集创建一块共享内存,即cursorwindow moveToPosition源码路径 fillWindow----真正耗时的地方 然后会执行sql语句,向共享内存中填入数据, fillWindow源码路径 在SQLite

org.apache.felix.framework-5.6.12源码解析——*framework中dto,hook,launch,namespace,startlevel部分

DTO 在Felix中使用了DTO模式,在传统的编程中,我们一般都是前台请求数据,发送到Webservice,然后WebService向数据库发出请求,获取数据,然后一层层返回:模型如下: 这种比较原始的请求方式带来的缺点有很多,多次请求耗费一定的网络资源,减慢效率.如果一次性返回整个实体类,还可能造成数据库表结构的泄漏. DTO模型: 这样带来的好处有: 1.依据现有的类代码,即可方便的构造出DTO对象,而无需重新进行分析. 2.减少请求次数,大大提高效率. 3.按需组织DTO对象,避免传输整

深入源码解析spring aop实现的三个过程

Spring AOP的面向切面编程,是面向对象编程的一种补充,用于处理系统中分布的各个模块的横切关注点,比如说事务管理.日志.缓存等.它是使用动态代理实现的,在内存中临时为方法生成一个AOP对象,这个对象包含目标对象的所有方法,在特定的切点做了增强处理,并回调原来的方法. Spring AOP的动态代理主要有两种方式实现,JDK动态代理和cglib动态代理.JDK动态代理通过反射来接收被代理的类,但是被代理的类必须实现接口,核心是InvocationHandler和Proxy类.cglib动态代