https://www.jianshu.com/p/5baa20d3d953
(我在简书上的文章)
目录
场景1
往Fragment里动态添加一个自定义View
final ToastView toastView = new ToastView(webViewFragment.getContext());
webViewFragment.getContentView().addView(toastView);
webViewFragmentController = new WebViewFragmentController(webViewFragment, toastView);
toastView.init(webViewFragment,toastParamsBean);
布局如下
需求就是当只显示tv_title时,rl_wrapper的宽高为wrapper,但是无论怎么测量啥的方法都试过了不行。
最终自己试出可行的代码,但是并不清楚什么个原理
/**
* 测量View设置具体大小(注意一二三步,一步也不能少。)
* @param view
*/
public static void measureViewWrapWrap(View view){
//第一步
ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
view.setLayoutParams(layoutParams);
//第二步
int w = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
int h = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
Logger.d(TAG, "measureView: " + "w = " + w + ",h = " + h);
view.measure(w, h);
//第三步
int mWidth = view.getMeasuredWidth();
int mHeight = view.getMeasuredHeight();
Logger.e(TAG, "measureViewWrapWrap: mWidth = " + mWidth + ",mHeight = " + mHeight);
ViewGroup.LayoutParams layoutParams2 = view.getLayoutParams();
layoutParams2.width = mWidth;
layoutParams2.height = mHeight;
view.setLayoutParams(layoutParams2);
}
场景2
wrap指定无效
https://blog.csdn.net/u010648159/article/details/81112643
https://www.jianshu.com/p/ca118d704b5e
现如今,作为一个5年程序猿,不能再得过且过,懵懵懂懂地写代码了。准备以这个问题为导向,深入研究一下Android动态布局相关的知识。
在
https://blog.csdn.net/jiangxuqaz/article/details/46941479
它里面有句话:"子View的widthMeasureSpec和heightMeasureSpec是由父View的widthMeasureSpec和heightMeasureSpec,LayoutParams和子ViewMarginLayoutParams决定的"
https://www.jianshu.com/p/08047f7f8fcf
这2篇文章里我好像找到了答案。
自定义View核心
核心一:Measure(测量)
1.https://www.jianshu.com/p/08047f7f8fcf (说的可能有点羞涩难懂,貌似是一种哲理提炼,但是最详细。最后的示例,也没有给出完整代码,只能他自己知道怎么回事。)
作者一直强调的重点
,"需求"一词,并不是作者独创,其实官方类的说明就是这么写的。
另外一个重点:LayoutParams,子View的Measure并不是独立的,而是和父View是息息相关的,通过LayoutParams向父类表达自己的期望,父类才知道Measure子View传递什么需求。
2.https://blog.csdn.net/a553181867/article/details/51494058
这篇分析源码,十分到位,思路特别地清晰,建议多看几遍。
上面这句话也正好解决了我博客最开始提出的疑问。
测量流程整理
从顶级View开始了测量流程,然后由外到内,逐级测量,调用View#measure方法(final修饰,不可更改),measure方法调用onMeasure方法(可以被复写,不同的View的实现基本不一样 )
- View的onMeasure方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
仅仅是保存了测量的宽高
- ViewGroup的onMeasure方法
和View一样,未做复写。 -
其它View的onMeasure方法
基本都会复写,基本都会结合自己本身的宽高、自己本身的布局参数、子View的LayoutParams去综合计算出子View的宽和高的MeasureSpec,然后调用View#measure方法对子View进行测量。
代码上表现为:
都有measureChild[XXX]方法(自己定义的或者复用ViewGroup的,FrameLayout就是复用ViewGroup的,RelativeLayout就是自定义的方法,measureChild[XXX]方法子类可能会复写),然后在这个方法内调用getChildMeasureSpec方法(自己定义的或者复用ViewGroup的)计算出子View的宽和高的MeasureSpec值。
子View的布局参数和测量大小,可能会导致当前View重新测量,如FrameLayout,看measureMatchParentChildren这个变量。
补充
但是,关于博客我有几点需要补充。
- 1.对于DecorView来说,它已经是顶层view了,没有父容器,那么它的MeasureSpec怎么来的呢
关于getRootMeasureSpec这个方法的第二个参数,一直追溯:lp ----> mWindowAttributes ----> WindowManager.LayoutParams
所以由此断定:DecorView的MeasureSpec的mode是EXACTLY,size就是窗口的宽高。
核心二:Layout(布局)
还是结合上面这位大神的博客分析https://blog.csdn.net/a553181867/article/details/51524527
和measure的形式差不多,子View也是通过复写onLayout达到不同的效果。
- View的onLayout方法
空实现,因为View不需要布局其它的View。
- ViewGroup的onLayout方法
抽象方法,这里有java语法有点特殊:View的onLayout是空实现,它的子类ViewGroup可以把它复写成抽象方法,而且方法域由protected变为public。
正因为ViewGroup没有复写onLayout方法,所以xml直接用ViewGroup嵌套View,View是不会显示的。
- 其它View的onLayout方法
根据子View的各种可能干扰布局位置的参数,子View的测量过的宽高,调用子View的layout方法进行布局。
补充
1.纠正一下博客的一个错误
View的layout并非是final的,而是DecorView没有复写这个方法。
核心三:Draw(绘制)
还是结合上面这位大神的博客分析
https://blog.csdn.net/a553181867/article/details/51570854
可以看出,View的Draw流程也比较复杂的。performDraw() ---> draw(boolean fullRedrawNeeded) ---> drawSoftware ---> mView.draw(canvas)
和measure、layout的形式差不多,子View也是通过复写onDraw达到不同的效果。如果View是透明的,就没有调用onDraw,因为透明的View没有绘制的意义。
- View的onDraw方法
空实现 - ViewGroup的onDraw方法
和View一样,未复写。 - 其它View的onDraw方法
一般都未复写
循环绘制机制
和measure、layout不同,draw的循环递归不是在onXXX方法里实现。
View的draw方法里会调用dispatchDraw(Canvas canvas)方法,View本身这个方法是空实现,但是ViewGroup作了实现,会调用drawChild方法,此方法会调用子View的draw(Canvas canvas, ViewGroup parent, long drawingTime)方法,进而再调用子View的draw(Canvas canvas)方法,如此循环递归。
View的绘制不仅仅是onDraw方法
结合上面和博客还有源码分析可知,onDraw只是绘制View的content(内容),并不能代表绘制的全部。draw的过程其实有很多步骤:
- 第1步:绘制背景
调用drawBackground(Canvas canvas)方法
这个方法会调用背景Drawable的draw方法,不同的Drawable会有不同的实现。
- 第3步:绘制内容
调用onDraw(Canvas canvas)方法
- 第4步:绘制子View
调用dispatchDraw(Canvas canvas)方法
- 第6步:绘制前景
调用onDrawForeground(Canvas canvas) 方法,
绘制滚动指示器:onDrawScrollIndicators(canvas);
绘制滚动条:onDrawScrollBars(canvas);
最后,和绘制背景类似,这个方法会调用前景Drawable的draw方法,不同的Drawable会有不同的实现。
自定义View实践
上面写了这么多,准备再写一篇事件分发,然后出一篇自定义View实践文章。
绘制冲突
1.ScrollView与ListView嵌套冲突的问题
ScrollView嵌套ListView,会导致ListView只显示一个Item的高度, 无论ListView的高度设置成任何的值(match、wrap、exactly)都如此。这里属于View的绘制冲突,就不属于事件冲突了,不要搞混了。
网上提供了各种解决办法,下面我从源码的角度分析分析实质的原因:
先感性地写一些Demo
获取测量模式的工具类
public class CUtil {
public static String getMeasureModeName( int measureSpec){
int result = View.MeasureSpec.getMode(measureSpec);
if(View.MeasureSpec.UNSPECIFIED == result){
return "UNSPECIFIED";
}else if(View.MeasureSpec.EXACTLY == result){
return "EXACTLY";
}else if(View.MeasureSpec.AT_MOST == result){
return "EXACTLY";
}
return "UNKNOWN";
}
}
自定义一个ListView,打印测量相关的日志。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Log.e(TAG, "onMeasure: " + CUtil.getMeasureModeName(heightMeasureSpec));
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int h = getMeasuredHeightAndState();
Log.e(TAG, "测量后的高度: " + h);
}
- Demo1:有ScrollView嵌套
<com.example.eventdispatchdemo.scrollView.MyScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/holo_blue_bright"
>
<com.example.eventdispatchdemo.scrollView.MyListView
android:id="@+id/listview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/holo_red_dark"
/>
</com.example.eventdispatchdemo.scrollView.MyScrollView>
输出日志:
MyListView: onMeasure: UNSPECIFIED
MyListView: 测量后的高度: 90
- Demo2:正常情况,只有ListView。
<com.example.eventdispatchdemo.scrollView.MyListView
android:id="@+id/listview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/holo_red_dark"
/>
输出日志:
MyListView: onMeasure: EXACTLY
MyListView: 测量后的高度: 2088
找到突破口分析
从上面的日志分析可知,ScrollView的嵌套导致了ListView的测量模式和大小的异常,所以直接分析ScrollView。真别说,我在ScrollView里立马找到一个方法可以解决问题:
public void setFillViewport(boolean fillViewport) {
if (fillViewport != mFillViewport) {
mFillViewport = fillViewport;
requestLayout();
}
}
默认mFillViewport是false,通过调用setFillViewport即可解决冲突问题。
但是不管给ListView设置什么高度属性,ListView的高度始终是match_parent的,因为ListView进行第二次测量。
- #### 分析ScrollView的测量源码(基于API28):
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (!mFillViewport) {
return;
}
//mFillViewport为true会执行下面的代码,将ChildView全部设置为和ScrollView一般的大小。
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode == MeasureSpec.UNSPECIFIED) {
return;
}
if (getChildCount() > 0) {
final View child = getChildAt(0);
final int widthPadding;
final int heightPadding;
final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (targetSdkVersion >= VERSION_CODES.M) {
widthPadding = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin;
heightPadding = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin;
} else {
widthPadding = mPaddingLeft + mPaddingRight;
heightPadding = mPaddingTop + mPaddingBottom;
}
final int desiredHeight = getMeasuredHeight() - heightPadding;
if (child.getMeasuredHeight() < desiredHeight) {
final int childWidthMeasureSpec = getChildMeasureSpec(
widthMeasureSpec, widthPadding, lp.width);
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
desiredHeight, MeasureSpec.EXACTLY);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
所以我们将目光放到
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
上,也就是FrameLayout的测量。通过打印MyScrollView的日志
MyScrollView: onMeasure: w-EXACTLY,h-EXACTLY,isFillViewPort:true
可以发现,ScrollView的宽高测量模式都是EXACTLY(因为布局是match_parent),而最终MyListView打印的高度测量模式是UNSPECIFIED,那么这中间又有怎样的故事?
到此处,可能有人就觉得没有必要再分析FrameLayout了,因为FrameLayout嵌套ListView不会有冲突。(其实这是个错误的理解,虽然ScrollView是调用的FrameLayout的onMeasure方法,但是onMeasure方法的运行载体还是ScrollView。)
接着看FrameLayout的测量方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
...
}
}
...
}
接着点击measureChildWithMargins方法,会跳转到ViewGroup,一直往下分析,想破了头也不可能得出“ScrollView的高度是EXACTLY,到了ListView的测量高度为UNSPECIFIED”的结论。
杜娘解决了我的疑惑:https://blog.csdn.net/u011247387/article/details/90759338
其实,ScrollView复写了measureChildWithMargins方法,只是代码跳转到了ViewGroup。
看ScrollView的measureChildWithMargins方法:
@Override
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin +
heightUsed;
final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
MeasureSpec.UNSPECIFIED);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
果然,ScrollView为子类指定高度的测量模式都是UNSPECIFIED。
那么问题就在于:
MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal
为什么只给ListView一行的item高度?
那这个值到底是多少,复写一下MyScrollView的measureChildWithMargins方法:
@Override
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int usedTotal = getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin +
heightUsed;
int ch = MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal;
Log.e(TAG, "getPaddingTop: " + getPaddingTop());
Log.e(TAG, "getPaddingBottom: " + getPaddingBottom());
Log.e(TAG, "lp.topMargin: " + lp.topMargin);
Log.e(TAG, "lp.bottomMargin: " + lp.bottomMargin);
Log.e(TAG, "ch: " + ch);
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
MeasureSpec.UNSPECIFIED);
Log.e(TAG, "ch2: " + MeasureSpec.getSize(childHeightMeasureSpec));
super.measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed);
}
对于API23以上的手机,注意上面的makeMeasureSpec应该是makeSafeMeasureSpec方法,只不过这个方法是hide,需要通过反射调用。
@Override
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int usedTotal = getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin +
heightUsed;
int ch = MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal;
Log.e(TAG, "getPaddingTop: " + getPaddingTop());
Log.e(TAG, "getPaddingBottom: " + getPaddingBottom());
Log.e(TAG, "lp.topMargin: " + lp.topMargin);
Log.e(TAG, "lp.bottomMargin: " + lp.bottomMargin);
Log.e(TAG, "ch: " + ch);
int childHeightMeasureSpec = 0;
try {
childHeightMeasureSpec = (int) ReflectManger.invokeMethod(MeasureSpec.class,null,"makeSafeMeasureSpec",new Class[]{Integer.class,Integer.class},
new Object[]{ Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
MeasureSpec.UNSPECIFIED}
);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
Log.e(TAG, "ch2: " + MeasureSpec.getSize(childHeightMeasureSpec));
super.measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed);
}
日志输出:
MyScrollView: onMeasure: w-EXACTLY,h-EXACTLY,i
MyScrollView: getPaddingTop: 0
MyScrollView: getPaddingBottom: 0
MyScrollView: lp.topMargin: 0
MyScrollView: lp.bottomMargin: 0
MyScrollView: ch: 1557
MyScrollView: ch2: 1557
MyListView: onMeasure: UNSPECIFIED,测量前的H size = 0
MyListView: 测量后的高度: 90
所以,ScrollView是无辜的,它传递给ListView的测量高度确实是1557,到了ListView,ListView测量前是0,测量后就成了90了。那么最有可能就是measure方法搞的鬼了,这个先放一放。
- #### 分析ListView的测量源码:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Sets up mListPadding
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int childWidth = 0;
int childHeight = 0;
int childState = 0;
mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED
|| heightMode == MeasureSpec.UNSPECIFIED)) {
final View child = obtainView(0, mIsScrap);
// Lay out child directly against the parent measure spec so that
// we can obtain exected minimum width and height.
measureScrapChild(child, 0, widthMeasureSpec, heightSize);
childWidth = child.getMeasuredWidth();
childHeight = child.getMeasuredHeight(); //测量ItemView的高度
...
}
...
if (heightMode == MeasureSpec.UNSPECIFIED) { //获取ListView的测量高度
heightSize = mListPadding.top + mListPadding.bottom + childHeight +
getVerticalFadingEdgeLength() * 2;
}
if (heightMode == MeasureSpec.AT_MOST) {
// TODO: after first layout we should maybe start at the first visible position, not 0
heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
}
setMeasuredDimension(widthSize, heightSize);
...
}
ListView的测量模式被ScrollView指定为UNSPECIFIED,那么ListView的高度就是由
if (heightMode == MeasureSpec.UNSPECIFIED) { //获取ListView的测量高度
heightSize = mListPadding.top + mListPadding.bottom + childHeight +
getVerticalFadingEdgeLength() * 2;
}
这段代码来决定的,ListView不设置额外的属性的话,heightSize就是等于childHeight的高度。
总结:
ScrollView与ListView嵌套冲突的问题,其实是一个共性的问题:本质上就是ScrollView强制为子View的高设置了UNSPECIFIED的测量模式,如果子View的高针对UNSPECIFIED测量模式做了特殊处理,就会发生奇怪的现象,我们习惯称之为冲突。
果不奇然,ScrollView嵌套其它的View也会出现问题,下面写几个Demo研究一下:
ScrollView嵌套RelativeLayout冲突示例1:
现象
》》MyRelativeLayout的布局高度为match_parent,MyRelativeLayout的不显示。
打印日志:
MyRelativeLayout: onMeasure: UNSPECIFIED,测量前的H size = 0
MyRelativeLayout: 测量后的高度: 0
》》将MyRelativeLayout的布局高度写死成100dp,MyRelativeLayout的高度显示正常。
打印日志:
MyRelativeLayout: onMeasure: UNSPECIFIED,测量前的H size = 0
MyRelativeLayout: 测量后的高度: 300
分析RelativeLayout的测量方法(没有子View的情况)
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
int height = 0;
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
final boolean isWrapContentHeight = heightMode != MeasureSpec.EXACTLY; //为true
... //中间省略子View的遍历
if (isWrapContentHeight) {
// Height already has top padding in it since it was calculated by looking at
// the bottom of each child view
height += mPaddingBottom;
//我给RelativeLayout设置了高度mLayoutParams.height > 0
if (mLayoutParams != null && mLayoutParams.height >= 0) {
height = Math.max(height, mLayoutParams.height);
}
height = Math.max(height, getSuggestedMinimumHeight());
height = resolveSize(height, heightMeasureSpec);
...
setMeasuredDimension(width, height);
}
RelativeLayout的高度模式为UNSPECIFIED,且没有子View,那么它的高度取决于它的布局参数heigth是否大于0。而MATCH_PARENT = -1,所以RelativeLayout的高度设置为MATCH_PARENT的时候它不显示,设置成一个常量它就显示。
ScrollView嵌套RelativeLayout冲突示例2:
》》MyRelativeLayout的布局高度还是match_parent,给MyRelativeLayout嵌套一个子View,子View的高度设置为MATCH_PARENT。子View和MyRelativeLayout都不显示。
输出日志:
MyRelativeLayout: onMeasure: UNSPECIFIED,测量前的H size = 0
MyView: onMeasure: UNSPECIFIED,测量前的H size = 0
MyView: 测量后的高度: 0
MyView: onMeasure: UNSPECIFIED,测量前的H size = 0
MyView: 测量后的高度: 0
MyRelativeLayout: 测量后的高度: 0
可见,UNSPECIFIED呈现了一定的传递性。
分析RelativeLayout的测量方法(有子View的情况)
请参照https://www.jianshu.com/p/8e3004494479
ScrollView嵌套View冲突示例:
》》MyRelativeLayout的布局高度还是match_parent,嵌套一个View(不是ViewGroup)。
View即使设置了高度,也不显示。
输出日志:
onMeasure: UNSPECIFIED,测量前的H size = 0
测量后的高度: 0
0 条评论