View layout源码分析

前言

本文主要分析了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的宽高。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容