Android自定义View基础之onLayout详解 素颜马尾好姑娘i 2022-07-14 04:22 446阅读 0赞 > 前两遍文章讲了一下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的上边的距离
还没有评论,来说两句吧...