图+源码,读懂View的Layout方法

简介: 本篇文章就带大家学习 View 绘制三大方法的第二个方法——Layout 方法。

前置知识

  • 有Android开发基础
  • 了解 View 体系
  • 了解 View 的 Measure 方法

前言

上文中,我们讲述了 View 里面的 Measure 方法,Measure 方法是页面绘制的三大方法中最为复杂的一个方法。它的 View 流程和 ViewGroup 的流程不尽相同,前者只需根据不同模式测量自身,而后者测量完自身后还需遍历测量子元素。并且他们在调用获取自身的 MeasureSpec 时候又会根据 DecorView 和普通 View 做不同的要求。

Layout 方法并没有如此复杂,相对来说较为简单。本篇文章就带大家学习 View 绘制三大方法的第二个方法——Layout 方法。

Layout 方法的作用和入口

Layout 一词翻译为:布局、布置。从这个英文翻译可以看出,Layout 方法与位置有关,事实的确如此,Layout 方法用于确定元素的位置所在

那么,Layout 方法的入口在哪里呢?在什么时候由什么方法调用呢?

这个问题在上一篇文章中也有提到,感兴趣的同学可以点击查阅。Layout 方法的入口与 Measure 方法类似,它是由 performLayout() 方法调用的,它的调用链是这种样子:performTraversals() -> performLayout() -> layout() 。我们可以在下方的代码中的注释1和2处看到 performLayout() 方法调用了 layout() 方法。

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
                           int desiredWindowHeight) {
    ...
    try {
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());//1
        ...
        if (numViewsRequestingLayout > 0) {
            ...
            if (validLayoutRequesters != null) {
                ...
                host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());//2
                ...
            }
        }
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
    mInLayout = false;
}
复制代码

Layout 流程

源码分析

下面,我们来看一下 View 中 layout 的源码,为了保留可读性,我把一些不讲解的代码注释掉了,且保留了源码的代码注释,感兴趣的同学可以阅读它的注释,会对它有更深的理解。

layout() 方法中,需要传入的 l t r b,其实分别对应着 left、top、right、bottom。即为从 左、上、右、下,View 相对于父布局的距离。对位置的确定的方法,主要在下面的注释1和2处。

/**
 * Assign a size and position to a view and all of its
 * descendants
 *
 * <p>This is the second phase of the layout mechanism.
 * (The first is measuring). In this phase, each parent calls
 * layout on all of its children to position them.
 * This is typically done using the child measurements
 * that were stored in the measure pass().</p>
 *
 * <p>Derived classes should not override this method.
 * Derived classes with children should override
 * onLayout. In that method, they should
 * call layout on each of their children.</p>
 *
 * @param l Left position, relative to parent
 * @param t Top position, relative to parent
 * @param r Right position, relative to parent
 * @param b Bottom position, relative to parent
 */
@SuppressWarnings({"unchecked"})
public void layout(int l, int t, int r, int b) {
    if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
        onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;
    boolean changed = isLayoutModeOptical(mParent) ?
        setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);//1
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);//2
        ...
    }
    ...
}
复制代码

接着我们看一下上面代码中注释1的代码。setFrame() 方法做了什么。我们从下面的代码逻辑以及注释中可以看到,这个方法是设置 View 的四个点的位置,并且会返回告知是否位置与之前有变更。执行完这段代码后,layout 就会执行 onLayout() 方法了,我也在下面给出代码。但是我们发现它是一个空方法,改方法的注释里面写道,我们要使用的时候需要重写这个方法。这是为什么呢?这是因为不同的控件有不同的实现,所以该方法就设定让子类去自行设计了。

/**
 * Assign a size and position to this view.
 *
 * This is called from layout.
 *
 * @param ...
 * @return true if the new size and position are different than the
 *         previous ones
 * {@hide}
 */
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
protected boolean setFrame(int left, int top, int right, int bottom) {
    boolean changed = false;
    ...
    if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
        changed = true;
        // Remember our drawn bit
        int drawn = mPrivateFlags & PFLAG_DRAWN;
        int oldWidth = mRight - mLeft;
        int oldHeight = mBottom - mTop;
        int newWidth = right - left;
        int newHeight = bottom - top;
        boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
        // Invalidate our old position
        invalidate(sizeChanged);
        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
        mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
        ...
        notifySubtreeAccessibilityStateChangedIfNeeded();
    }
    return changed;
}
/**
 * Called from layout when this view should
 * assign a size and position to each of its children.
 *
 * Derived classes with children should override
 * this method and call layout on each of
 * their children.
 * @param ...
 */
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
复制代码

而由于 ViewGroup 是继承  View 后对 layout()layout() 进行了简单的重写,这里便不再赘述。下面我们继续去看看 ViewGroup 的子类,看看 LinearLayout 是如何实现 onLayout() 方法的。我们可以看到,它是对该方法进行了重写,然后分别对两种不同的列表方向进行 layout 位置确定。

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    if (mOrientation == VERTICAL) {
        layoutVertical(l, t, r, b);
    } else {
        layoutHorizontal(l, t, r, b);
    }
}
复制代码

我们继续查看垂直的 layoutVertical() 方法,我们依旧是做了代码省略。从注释1和注释2处,我们可以看到 childTop 是不断在增大的,其实就是是实现了从上到下排序,后来的元素被排在原本元素的下面,而不会重叠。

注释3处的详细代码也已给出,setChildFrame() 方法其实就是调用子元素的 layout() 方法测量子寻找子元素的位置。这样子设计就可以层层传递,把整个 View 树的位置都寻找出来。

void layoutVertical(int left, int top, int right, int bottom) {
    ...
    for (int i = 0; i < count; i++) {
        final View child = getVirtualChildAt(i);
        if (child == null) {
            childTop += measureNullChild(i);//1
        } else if (child.getVisibility() != GONE) {
            final int childWidth = child.getMeasuredWidth();
            final int childHeight = child.getMeasuredHeight();
            final LinearLayout.LayoutParams lp =
                (LinearLayout.LayoutParams) child.getLayoutParams();
            ...
            if (hasDividerBeforeChildAt(i)) {
                childTop += mDividerHeight;//2
            }
            childTop += lp.topMargin;
            setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                          childWidth, childHeight);//3
            childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
            i += getChildrenSkipCount(child, i);
        }
    }
}
//注释3的详细代码
private void setChildFrame(View child, int left, int top, int width, int height) {
    child.layout(left, top, left + width, top + height);
}
复制代码

layout 的流程到此就讲完了。这里提一个小问题layout 流程是找出 View 的位置,那么 getWidth() 方法获得的位置是 layout 流程得出的位置吗?

一般来说,是同一个位置。layout 流程的位置是称为最终位置(或最终宽高),而 measure 流程的称为测量位置(或测量宽高) 。两者只是赋值时机不同。阅读这两篇文章后,我们分析流程,你会发现,getWidth()getMeasuredWidth() 两者得到的结果是一样的,我们也可认为测量宽高就是最终宽高。当然,如果对此重写了,就会不一致了。

public final int getWidth(){
    return mRight - mLeft;
}
复制代码

流程图

在此展示绘制的 layout 过程的流程图,希望能帮助理解该过程。

1.webp.jpg


相关文章
Jetpack Compose中ViewModel、Flow、Hilt、Coil的使用
Jetpack Compose中ViewModel、Flow、Hilt、Coil的使用
1993 0
Jetpack Compose中ViewModel、Flow、Hilt、Coil的使用
|
9月前
|
安全 Java 数据库
使用Java实现用户的注册和登录流程
以上提供了用户注册和登录的基本框架和必要的说明。在具体的应用场景中,可能还需结合框架特性如Spring Security等提供的高级特性来实现更为完备和安全的用户认证机制。在开发期间,务必注重代码的安全性、清晰性和可维护性,为用户资料保驾护航。
581 13
|
小程序 API 开发工具
Mpay: 真的找到啦,后台一直有同学想要解决个人免签收款的问题,这款专注于个人免签收款,轻量级且高效的支付解决方案
嗨,大家好,我是小华同学。mpay是一个基于微信支付官方SDK封装的库,简化了微信支付集成过程,支持公众号、扫码、小程序支付等场景。它提供简洁API、全面错误处理和灵活配置选项,适用于电商网站、线下实体店和移动应用,提升支付体验和运营效率。
725 58
|
SQL 关系型数据库 MySQL
sqlite3自动插入创建时间和更新时间
在本文中,作者分享了如何使用sqlite3数据库来记录结构化日志,并实现主键ID自增、插入数据时自动填充创建时间(created_at)以及更新数据时更新时间(updated_at)的功能。首先,创建数据库和表`position_info`,然后通过修改表结构使ID字段为自动递增。接着,设置`created_at`和`updated_at`字段默认值为当前时间。最后,创建一个触发器在数据更新时自动更新`updated_at`。完整SQL代码包括表创建和触发器定义。
678 0
|
XML JSON Java
Java中Log级别和解析
日志级别定义了日志信息的重要程度,从低到高依次为:TRACE(详细调试)、DEBUG(开发调试)、INFO(一般信息)、WARN(潜在问题)、ERROR(错误信息)和FATAL(严重错误)。开发人员可根据需要设置不同的日志级别,以控制日志输出量,避免影响性能或干扰问题排查。日志框架如Log4j 2由Logger、Appender和Layout组成,通过配置文件指定日志级别、输出目标和格式。
|
存储 Java API
动态代理实现的两种方式
【10月更文挑战第10天】
286 2
|
人工智能 小程序 前端开发
小程序源码|幼教小程序源码
小程序源码|幼教小程序源码
1354 4
|
SQL 前端开发 Java
又是大佬开源的一款自动预约i茅台APP的系统
这是一篇关于自动预约i茅台APP系统的介绍。该项目是一个开源系统,支持每日自动预约茅台,并且可以使用Docker一键部署。系统特性包括注册账号、添加用户、自动预约、选择预约门店、模拟位置等。提供了GitHub和B站上的视频教程,以及IDEA和Docker的启动指南。用户可以通过链接访问项目代码和文档,了解详细信息。
|
Android开发
Android 自定义View 测量控件宽高、自定义viewgroup测量
Android 自定义View 测量控件宽高、自定义viewgroup测量
820 0
|
存储 Android开发 C++
【Android 从入门到出门】第五章:使用DataStore存储数据和测试
【Android 从入门到出门】第五章:使用DataStore存储数据和测试
525 3

热门文章

最新文章