Android自定义View基础之onLayout详解

前两遍文章讲了一下MeasureSpec和onMeasure过程,那么现在就进行下一步,去layout的世界中喽一眼。
Layout的作用是ViewGroup用来确认子元素的位置,当ViewGroup的位置被确定后,它在onLyaout中会遍历所有的子元素并调用其layout方法,在layout方法中又会调用onLayout方法。
layout和onLayout区别:layout方法确定view本身的位置,onLayout确定所有子元素的位置。

View的layout源码

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);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    }

通过上面源码分析,其layout流程大致为:首先通过setFrame方法设定View的四个顶点的位置,即初始化mLeft mRight mBottom mTop四个值,这一步完成后,view在父容器中的位置就确定下来了。接着会调用onLayout方法,在onLayout中去循环遍历子元素,确定子元素的位置,通过查看View(不存在子元素)和ViewGroup的源码,显而易见,其onLayout方法都为空方法,需要子类根据自身需要去实现。

LinearLayout中onLayout源码

@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);
        }
    }

可以看到,和onMeasure方法一样,区分横纵布局进行分别的处理,下面以Vertical为例,进行描述。

void layoutVertical(int left, int top, int right, int bottom) {
        final int paddingLeft = mPaddingLeft;

        int childTop;
        int childLeft;

        // Where right end of child should go
        final int width = right - left;
        int childRight = width - mPaddingRight;

        // Space available for child
        // 计算子元素可使用的空间大小
        int childSpace = width - paddingLeft - mPaddingRight;
        // 获取子view的个数,进行下面的循环遍历
        final int count = getVirtualChildCount();

        final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
        final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
        // 根据布局属性,计算子元素的开始位置
        switch (majorGravity) {
           case Gravity.BOTTOM:
               // mTotalLength contains the padding already
               childTop = mPaddingTop + bottom - top - mTotalLength;
               break;

               // mTotalLength contains the padding already
           case Gravity.CENTER_VERTICAL:
               childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
               break;

           case Gravity.TOP:
           default:
               childTop = mPaddingTop;
               break;
        }
        // 开始遍历子元素
        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {
            // 在measure阶段形成的宽高
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();

                final LinearLayout.LayoutParams lp =
                        (LinearLayout.LayoutParams) child.getLayoutParams();

                int gravity = lp.gravity;
                if (gravity < 0) {
                    gravity = minorGravity;
                }
                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                                + lp.leftMargin - lp.rightMargin;
                        break;

                    case Gravity.RIGHT:
                        childLeft = childRight - childWidth - lp.rightMargin;
                        break;

                    case Gravity.LEFT:
                    default:
                        childLeft = paddingLeft + lp.leftMargin;
                        break;
                }

                if (hasDividerBeforeChildAt(i)) {
                    childTop += mDividerHeight;
                }

                childTop += lp.topMargin;
                // 设置子元素的位置,其内部调用子元素的layout方法
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

                i += getChildrenSkipCount(child, i);
            }
        }
    }

上面部分代码已做注释,大体整理一下其流程。通过遍历子元素并调用setChildFrame方法来为子元素指定对应的位置,其中childTop是不断增大的,也就代表着后面会越来越靠下,符合其纵向设置的特性。

setChildFrame源码

private void setChildFrame(View child, int left, int top, int width, int height) {        
    child.layout(left, top, left + width, top + height);
}

通过setChildFrame方法,子view会继续调用自己的layout方法,这样一步步确定自己的位置,这样一层一层的传递下去就完成了整个View树的layout过程。

getWidth与getMeasureWidth区别

final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
private void setChildFrame(View child, int left, int top, int width, int height) {        
    child.layout(left, top, left + width, top + height);
}

通过这几行代码,我们可以发现,width和height实际上就是子元素的测量宽高。而在layout方法中通过setFrame去设置四个顶点的时候,会进行如下的赋值:

mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;

现在,再看一下getWidth方法:

public final int getWidth() {
   return mRight - mLeft;
}

哇塞,发现什么没有,getWidth就是用的测量时候的宽度值。
结论:
1. 在View的默认实现中,View的测量宽高和最终宽高是相等的
2. 测量宽高形成于View的measure过程,最终宽高形成于layout过程,赋值时机不同
3. 在某些特殊情况下,这两个值也是有可能不会相等的,比如

@Override
    public void layout(int l, int t, int r, int b) {
        super.layout(l, t, r + 100, b + 100);
    }

上面代码中,最终宽高始终会比测量宽高大100px。
4. 在某些情况下,View需要多次measure才能确定自己的宽高,在前几次测量过程中,其得出的值可能和最终宽高不一致,但是最终来看,还是一样的
5. getMeasuredWidth()方法中的返回值是通过setMeasuredDimension()方法得到的,getWidth()方法中的返回值是通过View的右坐标减去其左坐标(right-left)计算出来的

ps:view.getLeft(),view.getRight(),view.getBottom(),view.getTop() 介绍
这四个方法用于获取子View相对于父View的位置
getLeft( )表示子View的左边距离父View的左边的距离
getRight( )表示子View的右边距离父View的左边的距离
getTop( )表示子View的上边距离父View的上边的距离
getBottom( )表示子View的下边距离父View的上边的距离

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值