Android常用Layout源码总结—FrameLayout

2,833 阅读8分钟

前言

通过学习Android官方Layout的源码,可以帮助自己更好的理解Android的UI框架系统,了解内部便捷的封装好的API调用,有助于进行布局优化和自定义view实现等工作。这里把学习结果通过写博客进行总结,便于记忆,不至于将来遗忘。

本篇博客中源码基于Android 8.1

FrameLayout特点

FrameLayout是Android开发中最常用的Layout之一,它的特点就是子view们是层叠覆盖,后添加的子view会覆盖在其他子view之上。

源码探究

构造函数

FrameLayout的构造函数很简单,处理一个FrameLayout的属性measureAllChildren:

public FrameLayout(@NonNull Context context, @Nullable AttributeSet attrs,
        @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);

    final TypedArray a = context.obtainStyledAttributes(
            attrs, R.styleable.FrameLayout, defStyleAttr, defStyleRes);

    if (a.getBoolean(R.styleable.FrameLayout_measureAllChildren, false)) {
        setMeasureAllChildren(true);
    }

    a.recycle();
}

measureAllChildren属性作用是设置是否在测量宽高时计算所有的子view。默认为false,即在measure阶段不会考虑状态为GONE的子view

LayoutParams

FrameLayout中定义了静态内部类LayoutParams继承自MarginLayoutParams,含有一个成员gravity:

public int gravity = UNSPECIFIED_GRAVITY;

因此支持子view设置父布局对齐方式。

测量onMeasure

由于FrameLayout帧布局的特点,它不像LinearLayout和RelativeLayout需要权重或相对关系等,只需要遍历子view,依次调用child测量,然后设置自身尺寸即可。但是也有细分不同情况,当FrameLayout的MeasureSpec模式为EXACTLY时,只需按常规流程进行即可。当模式为AT_MOST时,意味着FrameLayout自身尺寸不明确,需要反向依赖最大的那个child的尺寸,因此在遍历的同时需要记录最大尺寸。若同时存在child的LayoutParams设置了MATCH_PARENT,则意味着child又依赖父布局尺寸,因此在FrameLayout设置完自身尺寸后,需要再对它们进行一次测量。

FrameLayout中的宽高测量分为两部分。上部分为计算子view中的最大宽高,从而设置自身宽高。下部分为二次计算在上部分中未能精确计算宽高的子view的宽高,此时传给child的测量规格是根据FrameLayout测量后的宽高生成。

一、上部分:计算最大宽高

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();

	// 该变量用于判断是否记录需要二次测量子view(若FrameLayout的父布局给定的测量规格中未指明精确的大小,则为true)。
    final boolean measureMatchParentChildren =
            MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
            MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
    // mMatchParentChildren为一个ArrayList集合,用于缓存需要二次测量宽高的子view。
    mMatchParentChildren.clear();

    int maxHeight = 0;
    int maxWidth = 0;
    int childState = 0;

	// 遍历子view
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        // 判断child是否为GONE,或设置了measureAllChildren属性。
        if (mMeasureAllChildren || child.getVisibility() != GONE) {
        	// 调用child测量。
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            // child测量完成后,获取child的测量宽高值,并加上margin值,计算最大宽高值。
            maxWidth = Math.max(maxWidth,
                    child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
            maxHeight = Math.max(maxHeight,
                    child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
            // 合并child的测量状态。(getMeasuredState会获取child的测量状态。状态有宽state和高state,
            // 分别储存于mMeasuredWidth成员和mMeasuredHeight成员中的高8位。获取到储存的state后,
            // 将宽state设置在一个int中的第一字节位置,高state设置在第三字节位置,最后将这个组合好的int返回)
            childState = combineMeasuredStates(childState, child.getMeasuredState());
            if (measureMatchParentChildren) {
                if (lp.width == LayoutParams.MATCH_PARENT ||
                        lp.height == LayoutParams.MATCH_PARENT) {
                    // 若measureMatchParentChildren为true,且child的LayoutParams设置为填充父布局,
                    // 则需要加入List中,待FrameLayout计算完自身宽高后,再进行二次测量。
                    mMatchParentChildren.add(child);
                }
            }
        }
    }

    // Account for padding too
    // 增加计算padding
    maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
    maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

    // Check against our minimum height and width
    // 与最小宽高值比较。不能小于最小宽高值。
    maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
    maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

    // Check against our foreground's minimum height and width
    // 若存在前景图(与背景图相对的图),不能小于前景图的最小宽高值。
    final Drawable drawable = getForeground();
    if (drawable != null) {
        maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
        maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
    }

	// 设置FrameLayout自身的测量宽高值
	// (resolveSizeAndState方法会根据父布局给定的测量规格和自身计算出的宽高值判断返回一个新的宽高值,并在这个新的宽高值上设置MeasuredState)
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec,
                    childState << MEASURED_HEIGHT_STATE_SHIFT));

	// 下部分...
}

FrameLayout在onMeasure方法中,首先遍历了子view,进行子view测量,并比较出最大的子view的宽高值,同时还把没有精确设置宽高值的view加入列表缓存。然后用最大宽高值设置FrameLayout自身的宽高。

其中获取padding、获取Minimum宽高、组合尺寸值和状态等API为测量操作提供了极大便利,在自定义布局时,可以学习灵活调用。

MeasuredState补充说明:mMeasuredWidth和mMeasuredHeight这两个成员变量的高8位用于储存MeasuredState,其余24位储存尺寸值。有点类似MeasureSpec高2位储存模式,其余30位储存尺寸值。MeasuredState作用是,当view测量自身宽高时,若宽高值超过父布局给定的测量规格中的尺寸,则可以设置state为MEASURED_STATE_TOO_SMALL,请求父布局放宽尺寸限制。

二、下部分:二次测量子view 在完成了FrameLayout对自身宽高的计算后,再对列表中的子view进行二次测量。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	// 上部分...

	count = mMatchParentChildren.size();
    if (count > 1) {
    	// 至少有两个设置了MATCH_PARENT的子view时才执行二次测量。
        for (int i = 0; i < count; i++) {
            final View child = mMatchParentChildren.get(i);
            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

			// 重新给该child指定宽度测量规格
            final int childWidthMeasureSpec;
            if (lp.width == LayoutParams.MATCH_PARENT) {
            	// 宽度值为MATCH_PARENT时,获取FrameLayout自身的测量宽度值,减去padding和margin值,
            	// 计算新的宽度值(若小于0,取0),设置测量规格模式为EXACTLY,组合成新的测量规格。
                final int width = Math.max(0, getMeasuredWidth()
                        - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                        - lp.leftMargin - lp.rightMargin);
                childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                        width, MeasureSpec.EXACTLY);
            } else {
            	// 宽度值为精确的px、dp值或WRAP_CONTENT时,根据父布局传入的规格组合测量规格。
                childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                        getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                        lp.leftMargin + lp.rightMargin,
                        lp.width);
            }

			// 指定新的高度测量规格,逻辑同宽度规格。
            final int childHeightMeasureSpec;
            if (lp.height == LayoutParams.MATCH_PARENT) {
                final int height = Math.max(0, getMeasuredHeight()
                        - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
                        - lp.topMargin - lp.bottomMargin);
                childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                        height, MeasureSpec.EXACTLY);
            } else {
                childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                        getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                        lp.topMargin + lp.bottomMargin,
                        lp.height);
            }

			// 调用child再次测量。
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    }
}

在这部分中,遍历缓存集合中的子view,依次生成新的测量规格,之后调用子view再次测量。其中getChildMeasureSpec方法负责根据父布局传入的测量规格和padding和child的LayoutParams值生成新的测量规格。

getChildMeasureSpec方法介绍:

/**
 * @param spec 父布局给定的测量规格
 * @param padding padding和margin之和
 * @param childDimension LayoutParams中的width、height的值
 * @return 新的测量规格
 */
int getChildMeasureSpec(int spec, int padding, int childDimension)

该方法中首先从父布局测量规则中取出specMode和specSize,将specSize减去padding求出size(若小于0,则取0)。 之后根据specMode、size和childDimension结合条件判断生成新的测量规格:

specMode⬇️\childDimension➡️ MATCH_PARENT WRAP_CONTENT x px/dp
EXACTLY EXACTLY+size AT_MOST+size EXACTLY+childDimension
AT_MOST AT_MOST+size AT_MOST+size EXACTLY+childDimension
UNSPECIFIED UNSPECIFIED+size UNSPECIFIED+size EXACTLY+childDimension

注意:FrameLayout只有当至少有两个LayoutParams的width或height为MATCH_PARENT的子view时,才会遍历使用FrameLayout测量后的宽高值对这些子view进行二次测量。若仅有一个view,则是用FrameLayout的父布局传入的测量规格,对其进行测量,不再进行二次测量。

布局onLayout

FrameLayout的onLayout中根据父布局给定的上下左右,结合子view的gravity、宽高、margin等对子view进行布局。

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
    final int count = getChildCount();

	// 计算减去padding后的l、t、r、b
    final int parentLeft = getPaddingLeftWithForeground();
    final int parentRight = right - left - getPaddingRightWithForeground();

    final int parentTop = getPaddingTopWithForeground();
    final int parentBottom = bottom - top - getPaddingBottomWithForeground();

    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        // 跳过GONE的child
        if (child.getVisibility() != GONE) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

			// 获取child的测量宽高值
            final int width = child.getMeasuredWidth();
            final int height = child.getMeasuredHeight();

            int childLeft;
            int childTop;

            int gravity = lp.gravity;
            if (gravity == -1) {
            	// 默认对齐方式(左上角)
                gravity = DEFAULT_CHILD_GRAVITY;
            }

			// 获取布局方向(左至右或右至左)
            final int layoutDirection = getLayoutDirection();
            // 获取相对布局方向(针对Gravity.START和Gravity.END,根据布局方向转换成LEFT和RIGHT)
            final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
            // 取出垂直方向对齐方式
            final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

			// 判断水平方向的对齐方式
            switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                case Gravity.CENTER_HORIZONTAL:
                	// 水平居中
                	// 计算child左边位置(这里有加入计算margin,因此当左右margin不相等时,会有偏移,不完全居中)
                    childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                    lp.leftMargin - lp.rightMargin;
                    break;
                case Gravity.RIGHT:
                	// 靠右对齐
                	// 判断是否强制靠左对齐(默认false,即不强制)
                    if (!forceLeftGravity) {
                        childLeft = parentRight - width - lp.rightMargin;
                        break;
                    }
                case Gravity.LEFT:
                default:
                	// 靠左、默认对齐方式
                    childLeft = parentLeft + lp.leftMargin;
            }

			// 判断垂直方向对齐方式
            switch (verticalGravity) {
                case Gravity.TOP:
                    childTop = parentTop + lp.topMargin;
                    break;
                case Gravity.CENTER_VERTICAL:
                	// 垂直居中(逻辑与水平居中类似)
                    childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                    lp.topMargin - lp.bottomMargin;
                    break;
                case Gravity.BOTTOM:
                    childTop = parentBottom - height - lp.bottomMargin;
                    break;
                default:
                    childTop = parentTop + lp.topMargin;
            }

			// 调用child布局
            child.layout(childLeft, childTop, childLeft + width, childTop + height);
        }
    }
}

layoutChildren布局方法中,通过位运算从int中取出对齐方式,如果设置了START和END会根据RTL、LTR转换成对应的LEFT和RIGHT,之后依次判断水平方向对齐方式和垂直方向对齐方式。因为child的width、height确定,所以水平方向只需计算childLeft,垂直方向只需计算childTop即可。

总结

FrameLayout的核心逻辑即onMeasure和onLayout方法。onMeasure方法中在分发child测量的同时会比较child中的最大宽高值,并且当有child的LayoutParams设置了MATCH_PARENT,意味着他需要依赖父布局的尺寸,若父布局的测量模式不是指明明确的尺寸,则将该child添加至待测列表中。遍历完child后设置自身宽高。之后待测判断列表中缓存child的数量若至少2个,则使用FrameLayout自身的宽高重新生成测量规格,再调用child二次测量。 onLayout方法中遍历child,依次根据对齐方式,修改childLeft和childTop,最后调用child布局。