Android View 布局 过程

本文详细解析了Android中View的布局过程,包括如何确定View在界面的位置及尺寸,以及与测量的区别。阐述了layout方法的作用及其内部实现流程,并介绍了onLayout方法的用途。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

​ 当Android 的View测量完毕之后,才可以去布局。我们在测量时获取到的MeasuredHeightMeasuredWidth,在布局的时候就会用到,毕竟只有知道尺寸才好确定位置。

布局相对于测量来说就相对简单了很多。布局的过程就是确定View在界面显示位置的过程。View是一个矩形,决定一个View在界面显示的位置的信息主要是四个边到父View的边距,这里会有四个变量来记录各个边到父View的距离:

  • mLeft:View左边缘距离父View左部边缘的距离
  • mTop:View上边缘距离父View上部边缘的距离
  • mBottom:View底边缘距离父View上部边缘的距离
  • mRight:View右边缘距离父View左部边缘的距离

注意:这里的距离单位是像素;Android 屏幕的像素原点在屏幕的左上角,所以View的宽高计算如下:

View的宽=mRight-mLeft
View的高=mBottom-mTop

这里的宽高和测量来的MeasuredWidth和MeasuredHeight是不同的,不要混淆.看它们的源码:

测量得到的高

public final int getMeasuredHeight() {
    return mMeasuredHeight & MEASURED_SIZE_MASK;
}

View实际的高

public final int getHeight() {
    return mBottom - mTop;
}

width和MeasuredWidth代码跟上面的代码类似就不在贴了。

布局是绘制的前提,只有布局完毕才可以去绘制。View布局时调用的方法是layout(int l, int t, int r, int b)它的是个参数就是代表View四个边距离父View的距离。虽然View了layout 方法,但是不提倡去重写它,如果有需要我们应该去重写 onLayout(boolean changed, int left, int top, int right, int bottom),我们先看一下layout方法:

public void layout(int l, int t, int r, int b) {
    //判断onMeasure是否应该跳过,(变量mPrivateFlags3存着一些关于Layout的信息,应该是用来判断View是否测量过,具体的没研究过)
    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;
    //根据布局模式来判断是调用setOpticalFrame还是setFrame;
    //setOpticalFrame内部也会再次调用setFrame,所以不论isLayoutModeOptical是false还是true,都会调用setFrame
    //setFrame用来分配View的位置和大小,如果新传入的位置和尺寸与之前的位置和尺寸有差别就会返回true。
    //所以这里获取的是View的位置或者尺寸是否发生变化
    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        //如果View的位置或者尺寸发生变化或者满足(mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED,就会首先调用onLayout
        //onLayout是空的,没有任何实现,需要我们自己去实现。
        //凡是继承ViewGroup的类一般都要重写这方法,来确定其中的子view的位置和尺寸
        onLayout(changed, l, t, r, b);
        mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
        //这里设置 OnLayoutChangeListener 监听事件
        //ListenerInfo里面专门有个ArrayList来存储OnLayoutChangeListener
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLayoutChangeListeners != null) {
            //对ArrayList中的OnLayoutChangeListener中的事件监听器进行克隆
            ArrayList<OnLayoutChangeListener> listenersCopy =
                   (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
            int numListeners = listenersCopy.size();
            for (int i = 0; i < numListeners; ++i) {
              //遍历添加进来的事件监听器,依次调用onLayoutChange方法;这样layout就能在收到布局发生变化时得到响应了
                listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
            }
        }
    }
    mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
    mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}

onLayout(boolean changed, int left, int top, int right, int bottom)方法很简单,首先会判断一下是需要测量一下,如果不需要,记录一下传入的四个边到父View的边距;然后判断View的尺寸和位置是否变化过,如果变化去就调用onLayout方法,并且遍历OnLayoutChangedListener监听器。

onLayout(boolean changed, int left, int top, int right, int bottom)这个方法是空的,是需要子类去重写的。它的主要作用就是确定该View中子view的位置。通常会遍历该View中所有的子view,然后依次调用子view的layout方法,来确定子View的位置和尺寸。我们看一下FrameLayout中部分代码:

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
    //获取子View的个数
    final int count = getChildCount();
    //获取父View的四边
    final int parentLeft = getPaddingLeftWithForeground();
    final int parentRight = right - left - getPaddingRightWithForeground();
    final int parentTop = getPaddingTopWithForeground();
    final int parentBottom = bottom - top - getPaddingBottomWithForeground();
    //遍历所有的子view
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        //只有在子view可见并且不占据位置的时候才去设置子view的尺寸和位置
        if (child.getVisibility() != GONE) {
            //……省略子View在各个Gravity情况下四边距离屏幕边距的数值……
            child.layout(childLeft, childTop, childLeft + width, childTop + height);
        }
    }
}

所以我们在继承ViewGroup时,重写onLayout方法也可以做类似的处理。

​ 在上面我们提到布局变化监听,其实在View里提供了一个onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);当View的尺寸发生变化时就会调用这个方法。不过它刚加入的View树中时,它使用还之前的尺寸,都是0。

​ 在源码里,它是在 setFrame(int left, int top, int right, int bottom)中调用的。上面提到了setFrame是用来分配View的位置和大小,如果新传入的位置和尺寸与之前的位置和尺寸有差别就会返回true。简单看一下setFrame调用onSizeChanged的过程:

protected boolean setFrame(int left, int top, int right, int bottom) {
    boolean changed = false;
    //……省略log
    //新传入边距跟之前记录的边距不同,就说明View的尺寸或者位置发生变化
    if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
        changed = true;

        int drawn = mPrivateFlags & PFLAG_DRAWN;
        int oldWidth = mRight - mLeft;
        int oldHeight = mBottom - mTop;
        int newWidth = right - left;
        int newHeight = bottom - top;
        //根据新边距和旧边距,判断尺寸是否发生变化
        boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
        //重新绘图
        invalidate(sizeChanged);
        //将新边距赋值
        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
        mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
        mPrivateFlags |= PFLAG_HAS_BOUNDS;
        //如果尺寸发生变化就会调用sizeChange,onSizeChanged就是在sizeChange方法里调用的
        if (sizeChanged) {
            sizeChange(newWidth, newHeight, oldWidth, oldHeight);
        }
        if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
            mPrivateFlags |= PFLAG_DRAWN;
            invalidate(sizeChanged);
            invalidateParentCaches();
        }
        mPrivateFlags |= drawn;
        mBackgroundSizeChanged = true;
        if (mForegroundInfo != null) {
            mForegroundInfo.mBoundsChanged = true;
        }
        notifySubtreeAccessibilityStateChangedIfNeeded();
    }
    return changed;
}

private void sizeChange(int newWidth, int newHeight, int oldWidth, int oldHeight) {
    //调用onSizeChanged方法,这个方法是空的;如果有操作需要在尺寸发生变化时,可以重写这个方法
    onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);
    if (mOverlay != null) {
      mOverlay.getOverlayView().setRight(newWidth);
     mOverlay.getOverlayView().setBottom(newHeight);
   }
   rebuildOutline();
}

上面代码没有什么难度,在setFrame方法里根据新边距和旧边距,判断尺寸是否发生变化,如果变化就去调用sizeChange方法,onSizeChanged就是在sizeChange中调用的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值