都知道源码是好东西,一头扎进去往往容易淹死,楼主扔几个救生圈,剩下的看你们造化了。
问题:
1.为什么默认自定义View经常占满全屏(无论是设置wrap_content还是match_parent)?
2.为什么说view的最终测量尺寸是由view本身和其父容器共同决定,怎么决定的?
3.自定义View的状态保存。以及代码中new一个view 他的onSaveStateInstance会被调用么?
4.如何解决ScrollView嵌套中一个ListView的显示问题及滑动冲突?
1.写一个自定义View放入如下布局
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.test.fahionaly.customview2.MainActivity">
<com.test.fahionaly.customview2.ImgView2
android:id="@+id/id_imgView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="20dp"
>
</com.test.fahionaly.customview2.ImgView2>
<Button
android:id="@+id/id_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Fuck"
android:background="#991212"/>
</LinearLayout>
效果图:
图片大小是完全不可能占满屏幕的,但是你会发现没有看到下面的Button,你试试改成match_parent一样。为啥子?看源码!
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
跟进去
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
继续走 获取getDefaultSize的大小
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
你会发现只有确定他传入的两个参数才能确定自定义View的默认大小
那参数来自哪里?这就要追溯到View的根布局也就是Linearlayout的父布局DecorView。
我们的widthMeasureSpec和heightMeasureSpec应该从这里或者更上一层PhoneWindow传递进来,而PhoneWindow里面是有ViewRootImpl来具体实现果然我们在performTraversals中发现了痕迹
WindowManager.LayoutParams lp = mWindowAttributes;
----------
if (!mStopped || mReportNextDraw) {
boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
(relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
|| mHeight != host.getMeasuredHeight() || contentInsetsChanged) {
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
可以看到在performTraversals方法中通过getRootMeasureSpec获取原始的测量规格并将其作为参数传递给performMeasure方法处理。其中lp.width和lp.height均为MATCH_PARENT,其在mWindowAttributes(WindowManager.LayoutParams类型)将值赋予给lp时就已被确定。
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
然后把值带入最开始我们要探索的getDefaultSize方法你会发现无论设置wrap_content还是match_parent都是windowsize
那如何改才能露出我们的Button?
我的思路:重写onMeasure重点说一下wrap_content首先应该测试图片的大小,拿图片的宽高跟windowSize对比取小,毕竟要听爹爹的话。match_parent当然就是windowSize啦
下面上代码:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 声明一个临时变量来存储计算出的测量值
int resultWidth = 0;
// 获取宽度测量规格中的mode
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
// 获取宽度测量规格中的size
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
/*
* 如果爹心里有数
*/
if (modeWidth == MeasureSpec.EXACTLY) {
// 那么儿子也不要让爹难做就取爹给的大小吧
resultWidth = sizeWidth;
}
/*
* 如果爹心里没数
*/
else {
// 那么儿子可要自己看看自己需要多大了
resultWidth = mBitmap.getWidth()-getPaddingLeft();
/*
* 如果爹给儿子的是一个限制值
*/
if (modeWidth == MeasureSpec.AT_MOST) {
// 那么儿子自己的需求就要跟爹的限制比比看谁小要谁
resultWidth = Math.min(resultWidth, sizeWidth);
}
}
int resultHeight = 0;
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
if (modeHeight == MeasureSpec.EXACTLY) {
resultHeight = sizeHeight;
} else {
resultHeight = mBitmap.getHeight()+getPaddingTop()+getPaddingBottom();
if (modeHeight == MeasureSpec.AT_MOST) {
resultHeight = Math.min(resultHeight, sizeHeight);
}
}
// 设置测量尺寸
setMeasuredDimension(resultWidth, resultHeight);
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
下面来第二个问题:
2.为什么说view的最终测量尺寸是由view本身和其父容器共同决定,怎么决定的?
具体看一个方法-getChildMeasureSpec下面方法的主要作用是根据父容器的MeasureSpec同时结合View本身的LayoutParams来确定子元素的MeasureSpec(也就是说子元素的SPEC是由view本身和其父容器共同决定[原来理解为View自身layout决定 这个是错误的] )
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
看上面太繁琐?那么看主席书上的一个表格:
最后第三个问题
3.自定义View的状态保存。以及代码中new一个view 他的onSaveStateInstance会被调用么?
我们先看一下Activity如何保存状态如下图
没错就是使用onSaveInstanceState,当其他App切入前台时是由onSaveInstanceState来保存状态。
但是如果用户按下Back键显示关闭Activity是不会调用。onSaveInstanceState的。
那new出的view会执行onSaveInstanceState么?
楼主最初尝试是不会的于是乎查看了源码:
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) {
mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;
Parcelable state = onSaveInstanceState();
if ((mPrivateFlags & PFLAG_SAVE_STATE_CALLED) == 0) {
throw new IllegalStateException(
"Derived class did not call super.onSaveInstanceState()");
}
if (state != null) {
// Log.i("View", "Freezing #" + Integer.toHexString(mID)
// + ": " + state);
container.put(mID, state);
}
}
}
发现mID 跟SAVE_DISABLED_MASK必须要符合条件才行
那么在代码中如何做到?
.setID();
.setSaveEnable(true);
4.如何解决ScrollView嵌套中一个ListView的显示问题及滑动冲突?
测试发现ScrollView布局中嵌套Listview显示是不正常的,确切地说是只会显示ListView的第一个项。
要想解决这个问题我们依然需要看一下源码:
ScrollView.java的measureChildWithMargins()代码片段:
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
可以看出child的MeasureSpec是UNSPECIFIED
这时我们看一下ListView的onMeasure源码片段:
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();
if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = mListPadding.top + mListPadding.bottom + childHeight +
getVerticalFadingEdgeLength() * 2;
}
可以看到如果MeasureSpec是UNSPECIFIED高度只会拿第一个childHeight和一些padding值,所以我们确定是由于ScrollView的measureChildWithMargins导致ListView的Spec发生了变化。
那我们该如何做?
我的思路是重写ListView的onMeasure,改变MeasureSpec。
改成那种呢?再看一段源码:
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);
mWidthMeasureSpec = widthMeasureSpec;
}
measureHeightOfChildren这个是测量所有的子类,那就这个吧
我们改成At_Most,那我们势必要用到MeasureSpec.makeMeasureSpec这个方法这个方法两个参数
* @param size the size of the measure specification
* @param mode the mode of the measure specification
* @return the measure specification based on size and mode
由于MeasureSpec是Mode和Size组合而成,前2位是mode,后两位是size所以我们可以使用移位来实现,当然最后不能忘了设置。
最后代码如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int expandSepc = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSepc);
}
这样显示问题解决了,改撸一下嵌套滑动问题啦。
这又倒了老生常谈的事件传递机制
看完了流程我们开始撸源码,扒开ScrollView的onTouchEvent内衣
@Override
public boolean onTouchEvent(MotionEvent ev) {
`````省略无数代码
return true;
}
发现ACTION_MOVE时都是返回true即被消费掉 子布局如何得到垂帘呢?
解决方案:
两种思路~
1.外部拦截-即重写父布局onInterceptTouchEvent 当需要拦截的时候retrun true,相反就返回 false;
2.内部拦截-重写子布局onInterceptTouchEvent 内部不让父布局拦截
有点意思——子布局 ACTION_MOVE时调用getParent().requestDisallowInterceptTouchEvent(true) ACTION_UP/CANEL时getParent().requestDisallowInterceptTouchEvent(false)
大功告成。
综上几个例子是程序开发中经常让初学者摸不到头脑的问题,遇到问题如何冷静的分析问题至关重要,尤其是当stackoverflow上都没有的答案就需要媛猿们从源码中找答案了。除了复制粘贴程序员更应该做的是多问一个为什么,多点进去看看,多写一篇总结。不要让错误一遍又一遍的产生,能持之以恒的做到才是真的进步。