View的Layout源码分析

View的layout源码分析

我们在View的measure一文中学习了View的measure,这篇文章我们将来学习View的layout,就是View的摆放。我们先来看View,在看ViewGroup的。

单一View的layout

我们直接来看源码:

public void layout(int l, int t, int r, int b) {
        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方法来确定当前View自身的位置,并返回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的所有子View的位置。
        */
            onLayout(changed, l, t, r, b);
            if (shouldDrawRoundScrollbar()) {
                if(mRoundScrollbarRenderer == null) {
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                }
            } else {
                mRoundScrollbarRenderer = null;
            }
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
            ListenerInfo li = mListenerInfo;
            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) {
                    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);
        }
    }

方法的四个参数 l ,t ,r ,b分别是当前View从左,上,右,下相对于其父容器的距离。isLayoutModeOptical(mParent)返回出true就调用setOpticalFrame方法,false就调用setFrame方法。我们来看setOpticalFrame的源码:

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);
    }

我们发现,setOpticalFrame方法里面其实调用的是setFrame方法,我们再看这个方法源码:

protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;
        if (DBG) {
            Log.d("View", this + " View.setFrame(" + left + "," + top + ","
                    + right + "," + bottom + ")");
        }
        /**
        *判断当前VIew的,位置是否发现变化
        */
        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;// 当前View的位置发生变化的标志。
            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;
            if (sizeChanged) {
                sizeChange(newWidth, newHeight, oldWidth, oldHeight);
            }
            if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
                mPrivateFlags |= PFLAG_DRAWN;
                invalidate(sizeChanged);
                invalidateParentCaches();
            }
            mPrivateFlags |= drawn;
            mBackgroundSizeChanged = true;
            mDefaultFocusHighlightSizeChanged = true;
            if (mForegroundInfo != null) {
                mForegroundInfo.mBoundsChanged = true;
            }
            notifySubtreeAccessibilityStateChangedIfNeeded();
        }
        return changed;
    }

当前View的自身位置确定后,如果当前View的位置发生了变化,就会接着调用onLayout(changed, l, t, r, b)方法,我们来看View的onLayout方法的源码:

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

方法是空的,没有方法体。为什么呢?经过上面的介绍,layout方法确定了当前VIew的自身位置,那onLayout方法是干嘛用的呢?这个方法是用来确定当前View的所有子View的位置。这样我就知道View 的onLayout方法为什么是个空方法了,因为对于单一View来说,是没有子View的,所有不需要有方法。

总结

在这里插入图片描述

单一View的layout我们就学习到这。我们再看ViewGroup的。

ViewGroup的layout

我们来看ViewGroup的源码:

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);// 调用父View的ayout方法。
        } else {
            mLayoutCalledWhileSuppressed = true;
        }
    }

我们看到,在ViewGroup的layout方法中,调用他父View的layout方法,而ViewGroup是继承View的,所以最终会调用到View的layout方法中。所以ViewGroup也一样,在layout方法中,确定当前ViewGroup的自身的位置。若大小或位置发生了变化,接着会调用onLayout方法。我们在看ViewGroup的onLayout方法源码:

 protected abstract void onLayout(boolean changed,int l, int t, int r, int b);

是个抽象方法,即我们在自定义ViewGroup类型的view时,我们必须要重写这个onLayout方法。为什么这样设计呢?我们知道ViewGroup一般都是有子View,且不同的ViewGroup中的子View的摆放位置的方式不一样,比如LinearLayout,RelativeLayout,FrameLayout他们子View的摆放规则都不一样。所以我们在自定义ViewGroup类型的必须要重写这个onLayout方法,根据自己的要求来摆放子View的位置,把它定义为抽象的最好不过。

总结
自定义ViewGroup步骤:
1.计算自身ViewGroup的位置:layout()。
2.确定所有子View在当前ViewGroup的位置。onLayout()。根据自己的需要来计算出每个子View的位置,即子View在当前ViewGroup的位置(left,top,right,botton),然后调用子View的layout,完成子View的摆放。

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
     // 参数说明
     // changed 当前View的大小和位置改变了 
     // left 左部位置
     // top 顶部位置
     // right 右部位置
     // bottom 底部位置
     // 1. 遍历子View:循环所有子View
          for (int i=0; i<getChildCount(); i++) {
              View child = getChildAt(i);   
              // 2. 计算当前子View的四个位置值
              // 2.1 位置的计算逻辑
                ...// 需自己实现,也是自定义View的关键

              // 2.2 对计算后的位置值进行赋值
               int mLeft  = Left
               int mTop  = Top
               int mRight = Right
               int mBottom = Bottom
              // 3. 根据上述4个位置的计算值,设置子View的4个顶点:调用子view的layout() & 传递计算过的参数
              // 即确定了子View在父容器的位置
              child.layout(mLeft, mTop, mRight, mBottom);
              // 该过程类似于单一View的layout过程中的layout()和onLayout(),此处不作过多描述
          }
      }
  }

在这里插入图片描述
此处需注意:ViewGroup 和 View 同样拥有layout()和onLayout(),但二者不同的:

  • 一开始计算ViewGroup位置时,调用的是ViewGroup的layout()和onLayout();
  • 当开始遍历子View & 计算子View位置时,调用的是子View的layout()和onLayout()

ViewGroup的layout我们就学到这里。

View的测量宽/高和最终的宽/高有什么区别?

View的宽/高测量值的获取getMeasuredWidth()和getMeasuredHeight()
直接看源码:

/**
*获取测量的宽
*MEASURED_SIZE_MASK = 0x00FFFFFF;为什么要用这个值参与 与运算呢?
*0x00FFFFFF是16进制,32位。我们在介绍MeasureSpec时,高1和2位用来表示模式,而低30位是值,所以mMeasuredWidth与MEASURED_SIZE_MASK参与与运算。
*/
 public final int getMeasuredWidth() {
        return mMeasuredWidth & MEASURED_SIZE_MASK;
 }
 
/**
*获取测量的高
*MEASURED_SIZE_MASK = 0x00FFFFFF
*/
public final int getMeasuredHeight() {
        return mMeasuredHeight & MEASURED_SIZE_MASK;
}

mMeasuredWidth和mMeasuredHeight是在哪被赋值的呢?

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;
            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
    
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;
        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

我们自定义View的时候,在onMeasure()方法完成了自己的运算得到了测量值,在方法的最后调用setMeasuredDimensionRaw(int measuredWidth, int measuredHeight)方法来设置我们测量出来的测量值。

我们再看最终值的获取,看源码:

public final int getWidth() {
   return mRight - mLeft;
}

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

mLeft,mTop,mRight,mBottom分别是Viwe左,上,右,下相对于其父容器的距离。mRight - mLeft得大的是View的是Vew的实际宽值,mBottom - mTop得到的是View实际的高值。经过setFram方法的介绍,我们知道mLeft,mTop,mRight,mBottom是在这个setFram方法中得到赋值的。我们在自定义View的时候,在重写onLayout方法的时候,我们获取某个子View的测量宽高,然后根据要求计算出这个子View在这个ViewGroup的left,top,right,bottom,然后调用的这个子View的layout方法来完成这个子View的放置。

总结
在这里插入图片描述
即mMeasuredWidth,mMeasuredHeight的测量值是在onMeasure方法中测量完后调用setMeasuredDimension方法来赋值的来决定的。而实际的宽高是有layout方法是由参数的值通过运算得来的。日常开发中,mMeasuredWidth = mRight - mLeft , mMeasuredHeight = mBottom -mTop。

确实存在测量值和实际值不相等的情况。什么时候不相等呢?人为的情况下。从上面我们知道,我们在自定义View的时候,在onLayout方法中,根据子View的测量宽高值计算出了子View的left,right,top bottom,来设置子View在ViewGroup的中位置,实际也是设置了子View的实际宽高值,于是有
mMeasuredWidth = mRight - mLeft , mMeasuredHeight = mBottom -mTop。但是如果我们在计算出了子View的left,right,top bottom后,手动改了layout方法的参数值,这时mMeasuredWidth = mRight - mLeft , mMeasuredHeight = mBottom - mTop就不成立了,这时测量值和实际宽高值就不想等了。

public void layout( int l , int t, int r , int b){
   // 改变传入的顶点位置参数
   super.layout(l,t,r+100,b+100);
   // 如此一来,在任何情况下,getWidth() / getHeight()获得的宽/高 总比 getMeasuredWidth() / getMeasuredHeight()获取的宽/高大100px
   // 即:View的最终宽/高 总比 测量宽/高 大100px
}

上面代码没有任何意义,但是证明了测量宽/高的确可以不等于最终的宽/高。另外一种情况时在某些情况下,View需要多次的measure才能确定自己的测量宽/高,在前几次测量过程中,其得出测量宽/高有可能和最终宽/高不一致,但最终来说,测量宽/高和最终得宽/高相同。

上一篇: View的Measure过程源码分析
下一篇: View的draw源码分析

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android View 是 Android 中最基本的 UI 构建块之一,负责在屏幕上绘制视图并响应用户的操作。下面是一个简单的 View 源码分析过程: 1. 首先,我们需要了解 View 的继承关系。View 是 Android 中所有 UI 组件的基类,它的直接子类包括 ViewGroup、TextView、ImageView 等。其中,ViewGroup 又是各种布局容器的基类,例如 LinearLayout、RelativeLayout 等。 2. 接着,我们可以查看 View 的基本属性。这些属性包括 layout_width、layout_height、padding、background 等。其中,layout_width 和 layout_height 决定了 View 在布局中的大小,padding 指定了 View 的内边距,background 则是 View 的背景。 3. View 的绘制过程可以分为两个阶段:测量和绘制。在测量阶段,View 会根据其 layout_width 和 layout_height 等属性计算出自身的尺寸。在绘制阶段,View 会将自身绘制到屏幕上。 4. View 的事件响应机制是 Android 中 UI 开发的重要部分。当用户触摸屏幕时,系统会将事件传递给 ViewView 会根据自身的点击区域判断是否响应该事件,并将事件传递给其父容器或下一个 View 进行处理。 5. 最后,我们可以查看 View源码实现,深入了解 View 的内部实现逻辑。例如,View 的测量和绘制过程是通过 onMeasure 和 onDraw 方法实现的,事件响应机制是通过 onTouchEvent 和 dispatchTouchEvent 方法实现的。 总的来说,理解 Android View源码实现可以帮助我们更好地理解 Android UI 开发的工作原理,从而编写出更高效、更灵活、更具交互性的应用程序。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值