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>
很简单的布局文件,大家再脑海里尝试预览一下这个布局的效果。如果你认为是满屏黑色,那么这篇文章对你来说是有意义的。
因为这个布局预览出来是这样的:
黑色只有很短的一条,而这里本质的问题就在于TextView的onMeasure()方法。OK,今天的文章让咱们好好了解measure的过程…
从现象上来看,很明显咱们这个TextView在measure()的过程之后,就被认定为只有这个高。因此咱们今天就借这个case,好好来研究一番View在测量的过程中到底会受哪些因素影响。
开始前,咱们先不着急进入测量这部分。先进行一波准备工作,这一部分,咱们先来回顾一下[这前文章的内容]():
debug调用链,如下:
此时requestLayout(),由于parent是null,所以无从执行。而真正意义上的requestLayout()是下边的调用链。
首先根据官方文档我们能够明确:measure()的过程是自上而下的。
必须吐槽一下!这是Google开发者文档(国内的官网)…这翻译也是无语了。
有些小伙伴,可能并不了解View的测量过程,但是onMeasure()方法总还是多少有些涉猎吧?(不了解也没关系,这篇文章就是从一个小的demo,来聊一聊measure过程中的关键点)measure的流程可以简单用一个串行流程图表示:
OK,有了上述知识储备,我们就可以开启开篇那个效果的分析了:
requestLayout()方法会调用到View的measure()中,而measure()又会调用到自身的onMeasure()中。而measure()并不是一个可重写的方法,所以既然测量是自上而下,那咱们就从外围LinearLayout中的omMeasure()开始。
onMeasure()中比较简单,但是这里我们需要明确一下,这个方法的参数是什么含义:
/** * @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,是什么时机被设置的?
总结起来就是一句话:在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绑定到一起。
书归正传,measureChildWithMargins()方法中,同于父View的MeasureSpec和子View的LayoutParams来共通决定子View的MeasureSpec,然后调用子View的measure()方法。
这里一共包含了两个重点:
这部分逻辑主要在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开发艺术探索》中的那张图:
看了这个,咱们就可以思考一下咱们开篇遇到的问题:父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的size就应该是屏幕的size!从debug出来的结果也是如此:
可是开篇的实际效果已经否定了这个答案,那么问题出在哪呢?
既然在获取子View的MeasureSpec流程中我们已经明确是:parentSize + AT_MOST。不过咱们别忘了,咱们现在仅仅是获取了子View的MeasureSpec,有了MeasureSpec还需要一个最关键的一步:执行子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的测量宽高。
到此咱们可以大概有一个猜想:导致子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,那么直接看计算结果:
因此我们接下来调查的重点便是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的高度。
这里就不再基于源码展开了,有兴趣的同学可以自己追进去看一下,下边贴几张图来佐证这个结论:
<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"/>
这里咱们思考一个小问题:我们能不能做到不输入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)) } }
由上述分析,其实咱们明白这种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>
不贴图了,就是一屏黑....
这篇文章涉及的知识面着实有些不少,最开始属实没有预料到这篇文章会牵扯这么多精力。毕竟想要把众多知识点压缩到一篇文章中还是有些难度,何况自己还是一个彩笔。
希望这篇文章能给各位同学带来帮助吧,也欢迎大家留言一起讨论或者是分享给自己身边的好友~
今天的分享到此就结束了,感谢您的阅读,如果确实帮到您,您可以动动手指转发给其他人。
上一篇
已是最后文章
下一篇
已是最新文章