View layout源码分析

作者: GrayMonkey | 来源:发表于2017-08-27 20:28 被阅读52次

    前言

    本文主要分析了View的layout源码,总结了layout与onLayout在职能上的关系。

    View layout分析

      public void layout(int l, int t, int r, int b) {
           //1.位运算,取标志位,判断是否需要执行onMeasure方法,1表示执行
           //正常View的绘制流程中是不会执行的,取用测量缓存值,会导致onMeasure调用原因见后续说明
            if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
                onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
                //重置标志位为0,表示不执行
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }
    
            int oldL = mLeft;
            int oldT = mTop;
            int oldB = mBottom;
            int oldR = mRight;
            //2.判断View在父容器中的位置是否发生变化
            boolean changed = isLayoutModeOptical(mParent) ?
                    setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
            //3.如果View的位置发生变化或者PFLAG_LAYOUT_REQUIRED标志位为1将会执行onLayout方法,
           //在前面的measure分析中,onMeasure方法执行之后,会将该标志位置位1
            if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
                onLayout(changed, l, t, r, b);
                mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
               //监听回调,通知观察者,此处对mOnLayoutChangeListeners进行了一次深拷贝,保证线程安全,
               //可以推测存在其他线程操作mOnLayoutChangeListeners变量的可能
                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);
                    }
                }
            }
            //重置PFLAG_FORCE_LAYOUT标志位为0,表示不强制layout,与measure方法相关
            mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
           //标志位,表示至少执行过一次layout
            mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
        }
    

    注释1:
    SDK中对该标志位的注释如下:

      /**
         * Flag indicating that a call to measure() was skipped and should be done
         * instead when layout() is invoked.
         */
        static final int PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT = 0x8;
    //View的measure方法片段
     if (cacheIndex < 0 || sIgnoreMeasureCache) {
                    // measure ourselves, this should set the measured dimension flag back
                    onMeasure(widthMeasureSpec, heightMeasureSpec);
                    mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
             }  else {
                    //如果从测量缓存值中查到了本次测量规格,则PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT置为1,
    即onMeasure方法会在layout中调用
                    long value = mMeasureCache.valueAt(cacheIndex);
                    // Casting a long to int drops the high 32 bits, no mask needed
                    setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                    mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
                }
    

    我们知道正常的View的绘制流程,view的measure方法一定会得到调用,那么该标志位必定不会成立,至于在什么情况下会跳过measure方法而直接执行layout?阅读源码要点到为止!好吧,其实在源码中并没有找到答案,知道的朋友请科普,不胜感激。
    注释2:
    默认情况下,父容器的LayoutMode都为控件边界布局模式,所以默认情况下都会执行setFrame方法,下面进入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的位置发生变化,该函数将会返回true
            if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
                changed = true;
    
                // Remember our drawn bit
               //获取PFLAG_DRAWN标志位的值,表示是否需要绘制
                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 our old position
                invalidate(sizeChanged);
               //java层位置属性赋值,这就是为何需要在layout之后才能获得View的宽高的原因
                mLeft = left;
                mTop = top;
                mRight = right;
                mBottom = bottom;
              //native层位置属性赋值
                mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
    
                mPrivateFlags |= PFLAG_HAS_BOUNDS;
    
                //onSizeChange监听器回调,通常可以在该回调函数中获取View的宽、高,
               //因为位置属性在上面已经赋值了
                if (sizeChanged) {
                    sizeChange(newWidth, newHeight, oldWidth, oldHeight);
                }
                //如果View可见或者mGhostView不为空,PFLAG_DRAWN标志位强制置为1
                if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
                    // If we are visible, force the DRAWN bit to on so that
                    // this invalidate will go through (at least to our parent).
                    // This is because someone may have invalidated this view
                    // before this call to setFrame came in, thereby clearing
                    // the DRAWN bit.
                    mPrivateFlags |= PFLAG_DRAWN;
                    invalidate(sizeChanged);
                    // parent display list may need to be recreated based on a change in the bounds
                    // of any child
                    invalidateParentCaches();
                }
    
                // Reset drawn bit to original value (invalidate turns it off)
                mPrivateFlags |= drawn;
    
                mBackgroundSizeChanged = true;
                if (mForegroundInfo != null) {
                    mForegroundInfo.mBoundsChanged = true;
                }
                //Accessibility(盲人辅助功能)相关
                notifySubtreeAccessibilityStateChangedIfNeeded();
            }
            return changed;
        }
    

    可见setFrame方法是layout函数中的关键函数,它判断View的位置属性是否改变,并对其行赋值。
    思考
    既然layout已经完成了View的位置属性赋值,那么onLayout是用来干嘛的呢?其实在SDK的注释已经说明了一切

       /**
         * 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.
         */
        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        }
    

    一般而言,onLayout函数就是为ViewGroup而生,在onLayout中调用child的layout方法,达到摆放child的目的;正常的View是没有child的,所以View的onLayout方法为空实现。

    ViewGroup onLayout分析

    ViewGroup中覆写了layout方法,并将其定义为final,这样实现类将不能继承layout方法,只能继承onLayout,ViewGroup就应该专注于摆放child,不得不佩服google工程师的设计。下面就来简单看看FrameLayout的onLayout方法:

       @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) {
            final int count = getChildCount();
             //获取ViewGroup的padding值
            final int parentLeft = getPaddingLeftWithForeground();
            final int parentRight = right - left - getPaddingRightWithForeground();
            final int parentTop = getPaddingTopWithForeground();
            final int parentBottom = bottom - top - getPaddingBottomWithForeground();
            //遍历摆放child
            for (int i = 0; i < count; i++) {
                final View child = getChildAt(i);
                if (child.getVisibility() != GONE) {
                    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                    //获取的测量宽、高
                    final int width = child.getMeasuredWidth();
                    final int height = child.getMeasuredHeight();
    
                    int childLeft;
                    int childTop;
                    //默认gravity为左上
                    int gravity = lp.gravity;
                    if (gravity == -1) {
                        gravity = DEFAULT_CHILD_GRAVITY;
                    }
    
                    final int layoutDirection = getLayoutDirection();
                    final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                    final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
                       ............截取部分分析
                    //根据摆放规则计算childTop
                    switch (verticalGravity) {
                     
                        case Gravity.CENTER_VERTICAL:
                            childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                            lp.topMargin - lp.bottomMargin;
                            break;
                        case Gravity.BOTTOM:
                            childTop = parentBottom - height - lp.bottomMargin;
                            break;
                        default:
                            //可以将margin值设为负数来调整child在父容器中的layout位置
                            childTop = parentTop + lp.topMargin;
                    }
                    //关键点,摆放child
                    child.layout(childLeft, childTop, childLeft + width, childTop + height);
                }
            }
        }
    
    

    总结:
    View的layout与onLayout之间的关系不同于measure与onMeasure:
    1.layout方法通过setFrame方法用于确定自身的摆放位置,而onLayout方法则是用于摆放child,适用于ViewGroup,onLayout方法根据摆放规则计算出child的左上右下属性,通过调用child.layout方法实现child的摆放。有时候,我们可以通过设置child的margin值为负数,来达到调整child在容器中的位置,如LinearLayout设置为负数可以实现FrameLayout的叠层效果。
    2.measure方法扮演着一个测量优化的角色,真正的测量是放在onMeasure方法中,该方法不仅测量自身的宽高,还会测量child的宽高。

    相关文章

      网友评论

        本文标题:View layout源码分析

        本文链接:https://www.haomeiwen.com/subject/tfpiqxtx.html