Android之自定义View的死亡三部曲之(Layout)


前言

  • 大家好!本次我们将继续学习Android之自定义View的死亡三部曲中的第二部:排兵布阵

  • 我们在上一篇Android之自定义View的死亡三部曲之(Measure)中分析了死亡三部曲的第一部,也是三部中最复杂的一步:View的测量,想知道View的测量相关知识可以点进去查看哦!

  • 通过第一部View的测量,我们就能拿到View的三围数据了(View的宽高)。

  • 那么接下来我们要做的当然就是对测量好的View进行布局了。

  • Ok,说干就干,这次,我们同样是从ViewRootImpl的performTraversals方法开始,还记得我们的performTraversals方法体内部都有哪些内容么?我们再粘贴一下代码吧。

      private void performTraversals() {
          ...
            if (!mStopped) {
          //1、获取顶层布局的childWidthMeasureSpec
            int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);  
          //2、获取顶层布局的childHeightMeasureSpec
            int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
            //3、测量开始测量
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);       
            }
          } 
    
          if (didLayout) {
          //4、执行布局方法
            performLayout(lp, desiredWindowWidth, desiredWindowHeight);
            ...
          }
          if (!cancelDraw && !newSurface) {
           ...
          //5、开始绘制了哦
                performDraw();
            }
          } 
        ...
      }
    
  • 我们上次分析测量是以performMeasure为入口进行分析的,那么本次分析到布局,当然是从performLayout作为起点了。

  • Ok,那么我们就直接看performLayout方法体内部的源码吧

      private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
      int desiredWindowHeight) {
          mLayoutRequested = false;
          mScrollMayChange = true;
          mInLayout = true;
    
          final View host = mView;
          if (DEBUG_ORIENTATION || DEBUG_LAYOUT) {
                Log.v(TAG, "Laying out " + host + " to (" +
                        host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")");
          }
    
          Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
          try {
          //1、调用了host.layout
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); 
            mInLayout = false;
    
          .....
          } finally {
              Trace.traceEnd(Trace.TRACE_TAG_VIEW);
          }
          mInLayout = false;
      }
    
  • 我们可以看到,在1处,直接调用了host.layout进行布局,而host是什么东东呢?其实host就是我们的DecorView,还记得我们之前分析View的诞生之谜的时候,在创建ViewRootImpl时,直接把DecorView赋值给mView了。

  • 那么也就是说其实是调用了DecorView的layout方法。我们再看下其传递的参数分别是0,0,host.getMeasuredWidth(),host.getMeasuredHeight()

  • 而这四个参数按顺利所代码的含义分别是left,top,right,bottom,也就是左、上、右、下

  • left、top当然是0了,为什么呢?难道你想手机屏幕显示一个画面是,左边和顶部不是刚好贴合的么?显然不会希望这样,简直丑死啦。

  • 宽就是我们DecorView测量后的宽度,高就是DecorView测量后的高度


  • Ok,所有的控件当时都是继承自View了,那么我们看下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;
            //1、isLayoutModeOptical(mParent)判断是传统模式还是视觉模式,不懂的小伙伴可以百度一下哦
            //然后对不同模式分别调用对象的方法,作用是设置View的四个点
            boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    
            if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            //2、直接调用onLayout方法进行布局
            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 listenersCopy =
                      (ArrayList)li.mOnLayoutChangeListeners.clone();
              int numListeners = listenersCopy.size();
              for (int i = 0; i < numListeners; ++i) {
                  //3、如果设置了OnLayoutChangeListener,在layout之后就会回调告诉你了哦
                  listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
              }
          }
          }
    
          mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
          mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
      }
    
  • 在1中针对不同的layoutMode调用了不同的方法,我们来看下一班的layoutMode模式下调用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 + ")");
        }
        //1、如果有一个值发生了改变,那么就需要重新调用onLayout方法了,后面会分析到
        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;

            // Remember our drawn bit
            int drawn = mPrivateFlags & PFLAG_DRAWN;

            //2、保存旧的宽和高
            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            //计算新的宽和高
            int newWidth = right - left;
            int newHeight = bottom - top;
            //3、判断宽高是否有分生变化
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

            //Invalidate our old position
            //4、如果大小变化了,在已绘制了的情况下就请求重新绘制
            invalidate(sizeChanged);

            //5、存储新的值
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

            mPrivateFlags |= PFLAG_HAS_BOUNDS;

            if (sizeChanged) {
                //6、大小变化时进行处理
                sizeChange(newWidth, newHeight, oldWidth, oldHeight);
             }

            if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
                 //7、如果此时View是可见状态下,立即执行绘制操作
                  invalidate(sizeChanged);

                }

             mPrivateFlags |= drawn;

            mBackgroundSizeChanged = true;
            if (mForegroundInfo != null) {
                mForegroundInfo.mBoundsChanged = true;
              }

            notifySubtreeAccessibilityStateChangedIfNeeded();
        }
         return changed;
    }
  • 可以看到changed的值只与四个点是否发生了变化有关。
  • 同时,我们还发现,如果你想获得某个view的top、left、right、bottom的值,在layout之后就可以拿到了。
  • 而从View.layout方法的2位置处我们知道,在执行了setFrame之后调用的是onLayout方法,所以也就是说,我们可以在onLayout方法中获得四个位置点的值
  • View类的成员变量mLeft、mRight、mTop和mBottom分别用来描述当前视图的左右上下四条边与其父视图的左右上下四条边的距离,如果它们的值与参数left、right、top和bottom的值不相等,那么就说明当前视图的大小或者位置发生变化了。这时候View类的成员函数setFrame就会将参数left、right、top和bottom的值分别记录在成员变量mLeft、mRight、mTop和mBottom中。

  • 然后我们很开心的点开了View.onLayout方法,发现,居然是空的!~~空的!
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }
  • 没错,就是空的,一般该方法是用来确认childView的位置的,比如FrameLayout会调用onLayout方法告知childView,你可以可以开始布局了哦。然后childView就会调用自身的layout方法完成自身的布局工作,如果childView中还包含有childView,就会一直调用下去。

  • 我们先来梳理下流程:
    1、performTraversals内部调用performLayout开始执行布局工作
    2、performLayout内部会调用layout开始进行布局
    3、layout中会调用setFrame确定mTop,mLeft,mRight,mBottom的值以及判断是个点的值是否发生了变化
    4、最后调用onLayout方法通知下面的childView进行布局操作
  • ok,那么我们就分析下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 */);
}
  • 从上面可以看到内部只是调用了layoutChildren方法,layoutChildren才是具体的实现
  • 我们继续看下layoutChildren里面的代码:

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
    //1、获得子view的熟练
    final int count = getChildCount();
    //2、获得父view左面位置,getPaddingLeftWithForeground获得的是对应的内边距
    final int parentLeft = getPaddingLeftWithForeground();
    //3、获得父view右边位置
    final int parentRight = right - left - getPaddingRightWithForeground();
    //4、获得父view顶部位置
    final int parentTop = getPaddingTopWithForeground();
    //4、获得父view底部位置
    final int parentBottom = bottom - top - getPaddingBottomWithForeground();

    for (int i = 0; i < count; i++) {
        //5、遍历子view
        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;

            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;
             //6、针对不同的水平方向Gravity做处理
            switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                case Gravity.CENTER_HORIZONTAL:
                    childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                    lp.leftMargin - lp.rightMargin;
                    break;
                case Gravity.RIGHT:
                    if (!forceLeftGravity) {
                        childLeft = parentRight - width - lp.rightMargin;
                        break;
                    }
                case Gravity.LEFT:
                default:
                    childLeft = parentLeft + lp.leftMargin;
            }
            //6、针对不同的垂直方向Gravity做处理
            switch (verticalGravity) {
                case Gravity.TOP:
                    childTop = parentTop + lp.topMargin;
                    break;
                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:
                    childTop = parentTop + lp.topMargin;
            }
            //7、调用child的layout方法,对child进行布局,前面我们分析了
            child.layout(childLeft, childTop, childLeft + width, childTop + height);
        }
}
}
  • 知识点梳理:
    1、获取父View的内边距padding的值
    2、遍历子View,处理子View的layout_gravity属性、根据View测量后的宽和高、父View的padding值、来确定子View的布局参数,
    3、调用child.layout方法,对子View进行布局

对childView进行布局

  • 从上面的分析我们的可以知道,如果子view属于FrameLayout这种布局类的View,里面就会重复上面流程,如果不是,最终就会调用到View.onLayout,而这个方法是一个空的实现,所以我们在自定义View时,需要重新onLayout实现布局的操作

总结:

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

推荐阅读更多精彩内容