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

本贴最后更新于 2179 天前,其中的信息可能已经渤澥桑田

前言

  • 大家好!本次我们将继续学习 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 的四个点的数值,相对于之前的测量,是不是要简单一些呢?
  • Android

    Android 是一种以 Linux 为基础的开放源码操作系统,主要使用于便携设备。2005 年由 Google 收购注资,并拉拢多家制造商组成开放手机联盟开发改良,逐渐扩展到到平板电脑及其他领域上。

    334 引用 • 323 回帖
  • View
    11 引用 • 2 回帖
  • 学习

    “梦想从学习开始,事业从实践起步” —— 习近平

    169 引用 • 506 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...