View
的 onMeasure
先看普通 View
的测量方法
onMeasure
要关注那些内容:<LinearLayout
android:layout_width="400dp"
android:layout_height="wrap_content"
android:background="#3333"
android:orientation="vertical">
<com.python.cat.potato.view.custom.ItemView
android:layout_width="600dp"
android:layout_height="wrap_content"
android:background="#600f" />
<TextView
android:id="@+id/tv_test"
android:layout_width="700dp"
android:layout_height="80dp"
android:text="@string/task_count" />
LinearLayout>
View testView = view.findViewById(R.id.tv_test);
testView.post(() -> {
LogUtils.w("700dp==" + SizeUtils.dp2px(700));
LogUtils.w("testView " + testView.getWidth() + " , " + testView.getHeight());
ViewGroup parent = (ViewGroup) testView.getParent();
LogUtils.w("testView.parent " + parent.getWidth() + " , " + parent.getHeight());
});
输出log:
2018-12-19 23:51:28.699 7096-7096/ W/LogUtils: 700dp==1837
2018-12-19 23:51:28.700 7096-7096/ W/LogUtils : testView 1838 , 210
2018-12-19 23:51:28.700 7096-7096/W/LogUtils: testView.parent1050 , 236
通过日志可以看到系统控件并没有对这种情况进行处理,那么,自定义控件也就不用处理这种情况。
MeasureSpec.AT_MOST
的情况下,定义一个最小值,可以直接是0,或者是一个自定义的最小值,比如10dp.@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int fw = resolveMeasure(widthMeasureSpec, MIN_WIDTH);
int fh = resolveMeasure(heightMeasureSpec, MIN_HEIGHT);
LogUtils.d("fw=" + fw + " , " + fh);
setMeasuredDimension(fw, fh);
}
private int resolveMeasure(int lenMeasureSpec, int minLength) {
int len;
int mode = MeasureSpec.getMode(lenMeasureSpec);
int size = MeasureSpec.getSize(lenMeasureSpec);
switch (mode) {
case MeasureSpec.AT_MOST:
len = minLength;
break;
default:
len = size;
break;
}
return len;
}
在不重写 onLayout()
的前提下 ,setMeasuredDimension(fw, fh);
里面设置的大小,就是当前控件的尺寸大小了。
ViewGroup
的 onMeasure
对于容器而言,宽高往往跟里面的item
大小有关,而且,要考虑对item
设置margin
的支持。
measureChild
与 measureChildWithMargins
的区别简单来说,就是一句话: measureChildWithMargins
支持给child
设置 margin
,而measureChild
不支持。
细分的话,通过这两个方法测量的child
,在调用getMeasuredWidth/Height
的时候,获取的值是不一样的(前提是给child
设置了margin
)。
// 二者之间的关系可以用一行表达式来说明:
int measuredChildWidth = child.lp.marginLeft + child.lp.marginRight + measureChildWithMarginsWidth;
这两个方法都是ViewGroup.java
里面的实现方法。其实源码很短,可以直接看一下。
/**
* Ask one of the children of this view to measure itself, taking into
* account both the MeasureSpec requirements for this view and its padding.
* The heavy lifting is done in getChildMeasureSpec.
*
* @param child The child to measure
* @param parentWidthMeasureSpec The width requirements for this view
* @param parentHeightMeasureSpec The height requirements for this view
*/
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
/**
* Ask one of the children of this view to measure itself, taking into
* account both the MeasureSpec requirements for this view and its padding
* and margins. The child must have MarginLayoutParams The heavy lifting is
* done in getChildMeasureSpec.
*
* @param child The child to measure
* @param parentWidthMeasureSpec The width requirements for this view
* @param widthUsed Extra space that has been used up by the parent
* horizontally (possibly by other children of the parent)
* @param parentHeightMeasureSpec The height requirements for this view
* @param heightUsed Extra space that has been used up by the parent
* vertically (possibly by other children of the parent)
*/
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 childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
只看宽度,高度同理。对于宽度,
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width); // 这是 measureChild() 里面的宽度
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width); // 这是 measureChildWithMargins() 里面的宽度设置
可以看出,对于 带margin
的测量方法,比起不带margin
而言,多了
+ lp.leftMargin + lp.rightMargin + widthUsed
这样的一个数值。那么,这里的 widthUsed
应该写多少呢?
我一开始也不知道这里应该写多少,但是对比一下上面的方法就知道了,下面这个方法的目的是支持child
设置margin
,那么,widthUsed
这个变量就没什么作用了,直接设置为0即可。
但是这样说似乎也没有很强的说服力,看一下很多系统自带的容器控件,在使用这个方法的时候,给widthUsed
赋值为多少。几乎全部是0,heightUsed
也是一样,全部是0。
比如FrameLayout
,ActionBarView
等。
好,那对于自定义的ViewGroup
的 onMeasure
就好办了。
比如,我们自定义一个线性布局,竖直排列的。那么,onMeasure
就可以这样写:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int ws = MeasureSpec.getSize(widthMeasureSpec);
int wm = MeasureSpec.getMode(widthMeasureSpec);
int hs = MeasureSpec.getSize(heightMeasureSpec);
int hm = MeasureSpec.getMode(heightMeasureSpec);
int childCount = getChildCount();
int width = 0;
int height = 0;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// measureChild(child, widthMeasureSpec, heightMeasureSpec);
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
int measuredWidth = child.getMeasuredWidth();
int measuredHeight = child.getMeasuredHeight();
height += measuredHeight + lp.topMargin + lp.bottomMargin;
width = measuredWidth + lp.leftMargin + lp.rightMargin;
width = Math.min(width, ws);
}
int finalW = wm == MeasureSpec.EXACTLY ? ws : width;
int finalH = hm == MeasureSpec.EXACTLY ? hs : height;
com.apkfuns.logutils.LogUtils.i("measure: " + finalW + " , " + finalH);
setMeasuredDimension(finalW, finalH);
}
特别注意,这里面测量 child 的尺寸的目的还是为了设置自己的尺寸。
然后,既然测量的时候需要考虑 margin
,那么 布局的时候,也是一样。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
com.apkfuns.logutils.LogUtils.d(String.format("%s -- %s , %s , %s , %s", changed, l, t, r, b));
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// LogUtils.d(lp);
l = lp.leftMargin;
t = t + lp.topMargin;
r = l + child.getMeasuredWidth();
b = t + child.getMeasuredHeight();
child.layout(l, t, r, b);
t += child.getMeasuredHeight() + lp.bottomMargin;
// break;
com.apkfuns.logutils.LogUtils.d("item width:" + (lp.rightMargin + lp.leftMargin + child.getMeasuredWidth()));
}
}
这样,就在布局 child
的时候,设置上了对应的margin
。
注意:以上只是实现了对margin
的支持,至于 padding
并没有。
完成代码(添加了对自身 padding
的支持):
public class LineLayout extends ViewGroup {
public LineLayout(Context context) {
this(context, null);
}
public LineLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LineLayout(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public LineLayout(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
LogUtils.d("size w=" + w + " ,,, h=" + h);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int ws = MeasureSpec.getSize(widthMeasureSpec);
int wm = MeasureSpec.getMode(widthMeasureSpec);
int hs = MeasureSpec.getSize(heightMeasureSpec);
int hm = MeasureSpec.getMode(heightMeasureSpec);
int childCount = getChildCount();
int width = 0;
int height = 0;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// measureChild(child, widthMeasureSpec, heightMeasureSpec);
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
int measuredWidth = child.getMeasuredWidth();
int measuredHeight = child.getMeasuredHeight();
height += measuredHeight + lp.topMargin + lp.bottomMargin;
width = measuredWidth + lp.leftMargin + lp.rightMargin;
width = Math.min(width, ws);
}
int finalW = wm == MeasureSpec.EXACTLY ? ws : width + getPaddingLeft() + getPaddingRight();
int finalH = hm == MeasureSpec.EXACTLY ? hs : height + getPaddingTop() + getPaddingBottom();
com.apkfuns.logutils.LogUtils.i("measure: " + finalW + " , " + finalH);
setMeasuredDimension(finalW, finalH);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
com.apkfuns.logutils.LogUtils.d(String.format("%s -- %s , %s , %s , %s", changed, l, t, r, b));
t = getPaddingTop();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// LogUtils.d(lp);
l = lp.leftMargin + getPaddingLeft();
t = t + lp.topMargin;
r = l + child.getMeasuredWidth();
b = t + child.getMeasuredHeight();
child.layout(l, t, r, b);
t += child.getMeasuredHeight() + lp.bottomMargin;
// break;
com.apkfuns.logutils.LogUtils.d("item width:" + (lp.rightMargin + lp.leftMargin + child.getMeasuredWidth()));
}
}
@Override
protected MarginLayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
}