onLayout方法是ViewGroup中的一个抽象方法,继承ViewGroup的时候必须重写该方法,该方法是用于对子View进行布局的。
和onMeasure方法一样,onLayout也有个对应的layout方法。先看下layout和onLayout的区别
- layout方法是View中的方法,用来实现View的摆放,onLayout方法是ViewGroup中的方法,用来实现子View的摆放
- layout传入是个参数left、top、right、bottom是相对于父控件而言的,例如传入(20,20,50,70),该View的左上角位置对应的坐标是(20,20),宽为30,高为50;onLayout传入的参数l、t、r、f是更具父控件的实际可用空间来的(去除margin和padding的控件)
先看下layout方法
View中的layout方法
public void layout(int l, int t, int r, int b) {
//进行判断是否要调用onMeasure方法(所以onMeasure会被调用不止一次)
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
/**先判断isLayoutModeOptical方法返回结果,如果是true就执行
setOpticalFrame方法,如果是false就执行setFrame方法。
setOpticalFrame方法返回结果是执行setFrame方法返回的布尔值。
所以不管执行那个方法都会只想setFrame方法。
setFrame方法将新的left、top、right、bottom存储到View的成员变量中。然后根据返回的布尔值确认View的尺寸或者位置是否发生变化。
**/
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
//如果View的尺寸或者位置发生变化或者mPrivateFlags等于layout的标签PFLAG_LAYOUT_REQUIRED就执行以下代码
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//最先执行,在View中的onLayout是一个空实现的方法,用来实现View的摆放;在ViewGroup中onLayout是一个抽象方法,子类必须实现,调用layout方法循环摆放所有子View的位置
onLayout(changed, l, t, r, b);
if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
//执行完onLayout方法后,从mPrivateFlags中移除标签PFLAG_LAYOUT_REQUIRED
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
//存储各种监听器
ListenerInfo li = mListenerInfo;
//通过View的addOnLayoutChangeListener添加View位置和大小发生变化的监听器,将监听器存储在ListenerInfo中的mOnLayoutChangeListeners的集合中
if (li != null && li.mOnLayoutChangeListeners != null) {
//拷贝所有监听器
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
//遍历调用onLayoutChange方法,View的监听事件就得到了响应
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
}
private boolean setOpticalFrame(int left, int top, int right, int bottom) {
Insets parentInsets = mParent instanceof View ?
((View) mParent).getOpticalInsets() : Insets.NONE;
Insets childInsets = getOpticalInsets();
return setFrame(
left + parentInsets.left - childInsets.left,
top + parentInsets.top - childInsets.top,
right + parentInsets.left + childInsets.right,
bottom + parentInsets.top + childInsets.bottom);
}
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
View的layout方法
这是一个被final修饰的方法,意味着无法被子类重写。但是内部事件还是调用了View中的layout方法
@Override
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {
mTransition.layoutChange(this);
}
super.layout(l, t, r, b);
} else {
// record the fact that we noop'd it; request layout when transition finishes
mLayoutCalledWhileSuppressed = true;
}
}
再看下onLayout方法
View中的onLayout是一个空实现的方法,通过layout方法将新的left、top、right、bottom传给onLayout。
官方注释在此视图应该时从布局调用(layout方法执行),此视图(onLayout方法执行)给每个孩子分配一个大小和位置。子类的派生类应该重写此方法并在每个子View上调用布局(layout方法)。
View的子类当然是ViewGroup了,所以ViewGroup更加像是一个View的管理器,用来实现对子View的大小和位置变化进行控制。简单来说View会通过onLayout方法进行确认View的显示位置。
/**
* Called from layout when this view should
* assign a size and position to each of its children.
*
* Derived classes with children should override
* this method and call layout on each of
* their children.
* 当这个view和其子view被分配一个大小和位置时,被layout调用。
* @param changed 当前View的大小和位置改变了
* @param left 左部位置(相对于父视图)
* @param top 顶部位置(相对于父视图)
* @param right 右部位置(相对于父视图)
* @param bottom 底部位置(相对于父视图)
*
*/
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
ViewGroup中的onLayout方法是一个抽象方法,所以子类必须实现。并且调用View中的layout方法来确认View的位置和大小。
@Override
protected abstract void onLayout(boolean changed,int l, int t, int r, int b);
如何获取摆放子View后的位置
在layout方法中mLeft、mTop、mRight、mBottom这四个值是用来进行对View位置的摆放和大小的限制,是相对于父控件而言的。
如图白色区域是父View,黑色区域为子View。
- mLeft = view.getLeft() 子View左边界到父view左边界的距离
- mTop = view. getTop() 子View上边界到父view上边界的距离
- mRight = view. getRight() 子View右边界到父view右边界的距离
- mBottom = view. getBottom() 子View下边界到父view下边界的距离
public final int getLeft() {
return mLeft;
}
- view.getWidth() View的宽度,子View的右边界 - 子view的左边界
public final int getWidth() {
return mRight - mLeft;
}
- view.getWidth() View的高度,子View的下边界 - 子view的上边界
public final int getHeight() {
return mBottom - mTop;
}
- view.getMeasuredWidth() measure过程中返回的mMeasuredWidth
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
- view.getMeasuredHeight() measure过程中返回的mMeasuredHeight
public final int getMeasuredHeight() {
return mMeasuredHeight & MEASURED_SIZE_MASK;
}
getWidth()/Height和getMeasuredWidth()/getMeasuredHeight的区别?
针对getWidth()和getMeasuredWidth()进行分析,先看下区别
- getMeasuredWidth()是在measure()结束后得到的值,getWidth()是在layout()结束后得到的值
- getMeasureWidth()是通过setMeasuredDimension()方法来设置的,即getWidth()是通过视图右边的坐标减去左边的坐标计算出来的
分析下getWidth()和getMeasuredWidth()是如何赋值的
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
mMeasuredWidth这个值之前在小试牛刀-onMeasure方法中介绍过是View的实际大小宽对应的值是mMeasuredWidth,而mMeasuredWidth是通过setMeasuredDimension方法设置进来的。所以在measure方法结束后mMeasuredWidth才会有值,此时调用getMeasuredWidth可以获取对应的值。
public final int getWidth() {
return mRight - mLeft;
}
在getWidth()中是通过mRight - mLeft的计算返回的结果。而mRight和mLeft这两个值,上面有介绍是通过layout传递过来的。
简单写了个Demo
public class CustomViewGroup extends ViewGroup {
private static final String TAG = "CustomViewGroup";
int startX = 0;
int startY = 0;
boolean isMove = false;
public CustomViewGroup(Context context) {
this(context, null);
}
public CustomViewGroup(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec);
//20
Log.e(TAG, "getMeasuredWidth: " + getChildAt(0).getMeasuredWidth());
}
@SuppressLint("DrawAllocation") @Override
protected void onLayout(boolean changed, final int l, int t, int r, int b) {
View view1 = getChildAt(0);
View view2 = getChildAt(1);
view1.setBackgroundColor(getResources().getColor(R.color.colorAccent));
view2.setBackgroundColor(getResources().getColor(R.color.colorPrimary));
view1.layout(l, t, 200, 200);
view1.measure(0, 0);
view2.layout(view1.getWidth(), view1.getHeight(), view1.getWidth() + 200,
view1.getHeight() + 200);
initListener(view1);
initListener(view2);
}
@SuppressLint("ClickableViewAccessibility") private void initListener(final View view) {
view.setOnClickListener(new OnClickListener() {
@Override public void onClick(View v) {
//点击事件生效
}
});
view.setOnTouchListener(new OnTouchListener() {
@Override public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = (int) event.getX();
startY = (int) event.getY();
isMove = false;
break;
case MotionEvent.ACTION_MOVE:
isMove = true;
int moveX = (int) event.getX() - startX;
int moveY = (int) event.getY() - startY;
int left = view.getLeft() + moveX;
int top = view.getTop() + moveY;
int right = view.getRight() + moveX;
int bottom = view.getBottom() + moveY;
//限定边界
if (left <= 0) {
left = 0;
right = view.getWidth();
}
if (right >= getWidth()) {
left = getWidth() - view.getWidth();
right = getWidth();
}
if (top <= 0) {
top = 0;
bottom = view.getHeight();
}
if (bottom >= getHeight()) {
bottom = getHeight();
top = getHeight() - view.getHeight();
}
view.layout(left, top, right, bottom);
break;
case MotionEvent.ACTION_UP:
int l = view.getLeft();
int t = view.getTop();
int r = view.getRight();
int b = view.getBottom();
if (l <= getWidth() / 2 - view.getWidth() / 2) {
l = 0;
r = view.getWidth();
}
if (l > getWidth() / 2 - view.getWidth() / 2) {
r = getWidth();
l = getWidth() - view.getWidth();
}
view.layout(l, t, r, b);
if (!isMove) {
view.performClick();
}
break;
default:
}
return true;
}
});
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<com.test.ui.CustomViewGroup
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<View
android:layout_width="20px"
android:layout_height="wrap_content"/>
<View
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</com.test.ui.CustomViewGroup>
</LinearLayout>