布局的一般原则体现在哪些方面_布局图用什么软件做

(1) 2024-09-05 19:23

Hi,大家好,我是编程小6,很荣幸遇见你,我把这些年在开发过程中遇到的问题或想法写出来,今天说一说
布局的一般原则体现在哪些方面_布局图用什么软件做,希望能够帮助你!!!。

前言

友情提示:文章比较长,包括了大量代码和debug调用图。为了避免浪费大家的时间,开篇咱们看一段极为简单的代码(如果你能明确的解释这个想象,那么这篇文章没必要看下去):

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"     android:layout_width="match_parent"     android:layout_height="wrap_content"     android:orientation="vertical">     <TextView         android:layout_width="match_parent"         android:layout_height="match_parent"         android:background="@color/black" /> </LinearLayout> 

很简单的布局文件,大家再脑海里尝试预览一下这个布局的效果。如果你认为是满屏黑色,那么这篇文章对你来说是有意义的

因为这个布局预览出来是这样的:

布局的一般原则体现在哪些方面_布局图用什么软件做_https://bianchenghao6.com/blog__第1张

黑色只有很短的一条,而这里本质的问题就在于TextView的onMeasure()方法。OK,今天的文章让咱们好好了解measure的过程…

正文

从现象上来看,很明显咱们这个TextView在measure()的过程之后,就被认定为只有这个高。因此咱们今天就借这个case,好好来研究一番View在测量的过程中到底会受哪些因素影响。

一、我们布局中众多的View是怎么串起来的

开始前,咱们先不着急进入测量这部分。先进行一波准备工作,这一部分,咱们先来回顾一下[这前文章的内容]():

  • 1、我们setContentView的layout文件被LayoutInflate解析完,会以DecorView为parent,和DecorView关联起来。
  • 2、在Activity可见的流程中,Window会调用addView()传入DecorView,这其中会new一个ViewRootImpl,将DecorView加到ViewRootImpl中,同样以parent的形式。
  • 3、这样整个View便串起来了:ViewRootImpl -> DecorView -> FrmeLayout -> …
  • 4、而后在ViewRootImpl的addView(DecorView)时,会执行requestLayout()方法,开启measure、layout、draw的流程。

debug调用链,如下:

布局的一般原则体现在哪些方面_布局图用什么软件做_https://bianchenghao6.com/blog__第2张

此时requestLayout(),由于parent是null,所以无从执行。而真正意义上的requestLayout()是下边的调用链。

布局的一般原则体现在哪些方面_布局图用什么软件做_https://bianchenghao6.com/blog__第3张

二、如何理解View的测量

首先根据官方文档我们能够明确:measure()的过程是自上而下的。

布局的一般原则体现在哪些方面_布局图用什么软件做_https://bianchenghao6.com/blog__第4张

必须吐槽一下!这是Google开发者文档(国内的官网)…这翻译也是无语了。

有些小伙伴,可能并不了解View的测量过程,但是onMeasure()方法总还是多少有些涉猎吧?(不了解也没关系,这篇文章就是从一个小的demo,来聊一聊measure过程中的关键点)measure的流程可以简单用一个串行流程图表示:

布局的一般原则体现在哪些方面_布局图用什么软件做_https://bianchenghao6.com/blog__第5张


OK,有了上述知识储备,我们就可以开启开篇那个效果的分析了:

requestLayout()方法会调用到View的measure()中,而measure()又会调用到自身的onMeasure()中。而measure()并不是一个可重写的方法,所以既然测量是自上而下,那咱们就从外围LinearLayout中的omMeasure()开始。

三、LinearLayout的onMeasure()

onMeasure()中比较简单,但是这里我们需要明确一下,这个方法的参数是什么含义:

  • MeasureSpec就不用多说了,记录当前View的尺寸和测量模式
  • 另外明确一点,这里的MeasureSpec是父View的
/**  * @param widthMeasureSpec horizontal space requirements as imposed by the parent.  * @param heightMeasureSpec vertical space requirements as imposed by the parent.  */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {     if (mOrientation == VERTICAL) {         measureVertical(widthMeasureSpec, heightMeasureSpec);     } else {         measureHorizontal(widthMeasureSpec, heightMeasureSpec);     } } 

这里咱们就选measureVertical()追进去,方法里的边界条件非常的多,但其中对于子View的测量过程比较的简单,遍历所有的子View,挨个调用measureChildBeforeLayout()方法,而这个方法最终会走到ViewGroup中的measureChildWithMargins():

protected void measureChildWithMargins(View child,         int parentWidthMeasureSpec, int widthUsed,         int parentHeightMeasureSpec, int heightUsed) {     // 这个方法主要就是做了一件事情:通过子View的LayoutParams和父View的MeasureSpec来决定子View的MeasureSpec     final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();     final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,             mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin                     + widthUsed, lp.width);     final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,             mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin                     + heightUsed, lp.height);     child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } 

这里边可能会有一些同学有疑问:子View的LayoutParams是什么时候设置的?

四、View的LayoutParams,是什么时机被设置的?

这里咱们就插空解决一下标题的问题:View的LayoutParams,是什么时机被设置的?

总结起来就是一句话:在LayoutInflate中解析xml中设置的。具体什么样?直接上代码:

void rInflate(XmlPullParser parser, View parent, Context context,             AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {     // 省略部分代码     final View view = createViewFromTag(parent, name, context, attrs);     final ViewGroup viewGroup = (ViewGroup) parent;     final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);     rInflateChildren(parser, view, attrs, true);     viewGroup.addView(view, params); } public LayoutParams generateLayoutParams(AttributeSet attrs) {     return new LayoutParams(getContext(), attrs); } public LayoutParams(Context c, AttributeSet attrs) {     TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);     setBaseAttributes(a,             R.styleable.ViewGroup_Layout_layout_width,             R.styleable.ViewGroup_Layout_layout_height);     a.recycle(); }

上述代码描述比较清晰,说白了就是解析xml中,基于这个View在布局中的layout_width、layout_height属性来生成对应的LayoutParams。然后在通过addView()方法将View和LayoutParams绑定到一起。

五、子View的measure()

书归正传,measureChildWithMargins()方法中,同于父View的MeasureSpec子View的LayoutParams来共通决定子View的MeasureSpec,然后调用子View的measure()方法。

这里一共包含了两个重点:

  • 生成子View的MeasureSpec
  • 执行子View的measure()方法

5.1、生成子View的MeasureSpec

这部分逻辑主要在getChildMeasureSpec()方法中,我们直接追进去就好了:

 public static int getChildMeasureSpec(int spec, int padding, int childDimension) {     // 省略部分初始化代码     switch (specMode) {          case MeasureSpec.EXACTLY:              if (childDimension >= 0) {                  resultSize = childDimension;                 resultMode = MeasureSpec.EXACTLY;             } else if (childDimension == LayoutParams.MATCH_PARENT) {                 resultSize = size;                 resultMode = MeasureSpec.EXACTLY;             } else if (childDimension == LayoutParams.WRAP_CONTENT) {                 resultSize = size;                 resultMode = MeasureSpec.AT_MOST;             }             break;         case MeasureSpec.AT_MOST:              if (childDimension >= 0) {                 resultSize = childDimension;                 resultMode = MeasureSpec.EXACTLY;             } else if (childDimension == LayoutParams.MATCH_PARENT) {                 resultSize = size;                 resultMode = MeasureSpec.AT_MOST;             } else if (childDimension == LayoutParams.WRAP_CONTENT) {                 resultSize = size;                 resultMode = MeasureSpec.AT_MOST;             }             break;         case MeasureSpec.UNSPECIFIED:              if (childDimension >= 0) {                 resultSize = childDimension;                 resultMode = MeasureSpec.EXACTLY;             } else if (childDimension == LayoutParams.MATCH_PARENT) {                 resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;                 resultMode = MeasureSpec.UNSPECIFIED;             } else if (childDimension == LayoutParams.WRAP_CONTENT) {                 resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;                 resultMode = MeasureSpec.UNSPECIFIED;             }             break;         }     return MeasureSpec.makeMeasureSpec(resultSize, resultMode); } 

这部分代码,就是Google定的规则,也没什么好说的。总结起来就是《Android开发艺术探索》中的那张图:

布局的一般原则体现在哪些方面_布局图用什么软件做_https://bianchenghao6.com/blog__第6张

看了这个,咱们就可以思考一下咱们开篇遇到的问题:父View(LinearLayout)是wrap_content,子View(TextView)是match_parent,那么子View的MeasureSpec是什么样子?

有了上边的分析,我们很容易得出答案:parentSize + AT_MOST。因此咱们就知道这种场景下,子View的match_parent意味自己的宽高就是父View的宽高。那么此时父View的宽高是多少呢?

由于这里的父View已经是根View了,那么它的外边便是DecorView,而DecorView的MeasureSpec相对简单些,直接基于Window的宽高和自身的LayoutParams进行计算。

private static int getRootMeasureSpec(int windowSize, int rootDimension) {     int measureSpec;     switch (rootDimension) {         case ViewGroup.LayoutParams.MATCH_PARENT:             measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);             break;         case ViewGroup.LayoutParams.WRAP_CONTENT:             measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);             break;         default:             measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);             break;     }     return measureSpec; }

而DecorView的LayoutParams也很明确,看过setContentView代码的同学应该都比较清楚:

public void setContentView(View view) {     setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); }

因此这种场景下,DecorView的MeasureSpec是屏幕宽高 + EXACTLY,那么父View(LinearLayout)的宽高就很明确了:parentSize + AT_MOST。

是不是发现问题了?

  • 子View(TextView)的MeasureSpec是parentSize + AT_MOST
  • 父View(LinearLayout)的MeasureSpec是parentSize + AT_MOST
  • DecorView的MeasureSpec是屏幕的size + AT_MOST

有了上述的推导:子View的size就应该是屏幕的size!从debug出来的结果也是如此:

布局的一般原则体现在哪些方面_布局图用什么软件做_https://bianchenghao6.com/blog__第7张

可是开篇的实际效果已经否定了这个答案,那么问题出在哪呢?

既然在获取子View的MeasureSpec流程中我们已经明确是:parentSize + AT_MOST。不过咱们别忘了,咱们现在仅仅是获取了子View的MeasureSpec,有了MeasureSpec还需要一个最关键的一步:执行子View的measure()方法

5.2、执行子View的measure()方法

接下来咱们去看一看子View的measure()方法,上述的部分我们已经知道measureChildWithMargins()方法中会基于父View的MeasureSpec和子View的LayoutParams计算子View的MeasureSpec然后调用子View的measure():

protected void measureChildWithMargins(View child,         int parentWidthMeasureSpec, int widthUsed,         int parentHeightMeasureSpec, int heightUsed) {     // 省略获取子View的MeasureSpec的过程     child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }

通过断点的调用链我们可以看到,子View的measure()会调用到子View的onMeasure()中,然后通过setMeasureDimension()最终定下View的测量宽高。

布局的一般原则体现在哪些方面_布局图用什么软件做_https://bianchenghao6.com/blog__第8张

到此咱们可以大概有一个猜想:导致子View(TextView)最终高度不是parentSize的原因,极可能是因为自身的onMeasure()方法

走进onMeasure()方法中,我们会发现TextView的onMeasure()方法实现比较长,因此这里主要抽取关键逻辑:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {     // 省略部分代码     if (heightMode == MeasureSpec.EXACTLY) {         height = heightSize;         mDesiredHeightAtMeasure = -1;     } else {         int desired = getDesiredHeight();         height = desired;         mDesiredHeightAtMeasure = desired;         if (heightMode == MeasureSpec.AT_MOST) {             height = Math.min(desired, heightSize);         }     }     // 省略部分代码     setMeasuredDimension(width, height); } 

我们的子View(TextView)已经确定是AT_MOST,那么直接看计算结果:

布局的一般原则体现在哪些方面_布局图用什么软件做_https://bianchenghao6.com/blog__第9张

因此我们接下来调查的重点便是getDesiredHeight()方法:

private int getDesiredHeight() {     return Math.max(         getDesiredHeight(mLayout, true),         getDesiredHeight(mHintLayout, mEllipsize != null)); } // 这其中又间接的调到了Layout中的这个方法 layout.getHeight() // 而这个方法的实现,就是用一行的height * 所有文字的行数 public int getHeight() {     return getLineTop(getLineCount()); }

可以看到,这种场景下,TextView的高度是期望mLayout或mHintLayout中max的那个,而这个也是TextView特有的逻辑

OK,看过上面代码注释的同学,到这里应该就恍然大悟了。开篇的那一条黑条就是一行文本的高度。而这个高度就是TextView默认Paint的高度。

这里就不再基于源码展开了,有兴趣的同学可以自己追进去看一下,下边贴几张图来佐证这个结论:

布局的一般原则体现在哪些方面_布局图用什么软件做_https://bianchenghao6.com/blog__第10张

<TextView     android:layout_width="match_parent"     android:background="@color/black"     android:text="11"     android:textColor="@color/white"     android:layout_height="match_parent"/>
<TextView     android:layout_width="match_parent"     android:background="@color/black"     android:textSize="60sp"     android:layout_height="match_parent"/> 
布局的一般原则体现在哪些方面_布局图用什么软件做_https://bianchenghao6.com/blog__第11张

六、延伸问题1

这里咱们思考一个小问题:我们能不能做到不输入text,就让TextView占满全屏呢?答案是肯定,因为这篇文章主要就是在聊对这个问题的理解。

咱们已经明确这种case下,子View的MeasureSpec是parentSize + AT_MOST,改变最终measure()结果的是onMeasure(),那么我们直接重写onMeasure()…

class TestTextView @JvmOverloads constructor(     context: Context,     attrs: AttributeSet? = null ) : TextView(context, attrs) {     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {         setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec))     } } 

六、延伸问题2

由上述分析,其实咱们明白这种case下:子View的MeasureSpec = parentSize + AT_MOST。由于TextView本身复写了measure()才出现了开篇的效果。那么如果我们用View来替换TextView是不是就能够撑满全屏了?答案是肯定的:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"     android:layout_width="match_parent"     android:layout_height="wrap_content">     <View         android:layout_width="match_parent"         android:layout_height="match_parent"         android:background="#000000" /> </LinearLayout> 

不贴图了,就是一屏黑....

尾声

这篇文章涉及的知识面着实有些不少,最开始属实没有预料到这篇文章会牵扯这么多精力。毕竟想要把众多知识点压缩到一篇文章中还是有些难度,何况自己还是一个彩笔。

希望这篇文章能给各位同学带来帮助吧,也欢迎大家留言一起讨论或者是分享给自己身边的好友~

今天的分享到此就结束了,感谢您的阅读,如果确实帮到您,您可以动动手指转发给其他人。

上一篇

已是最后文章

下一篇

已是最新文章

发表回复