美文网首页view
View的绘制流程

View的绘制流程

作者: isLJli | 来源:发表于2021-03-09 15:37 被阅读0次

1.自定义View

自定义View可以分为三个流程:测量、布局、绘制 分别对应着onMeasure、onLayout、onDraw方法。

自定义View可以分为两种类型:
1.自定义ViewGroup :主要是onMeasure()、onLayout()测量和布局方法。
2.自定义View:主要是onMeasure()、onDarw()测量和绘制方法。

  • 在ViewGroup和View中都使用了测量onMeasure()方法,接下来通过一个写一个自定义一个瀑布流ViewGroup学习onMeasure()是怎么测量的。

2.onMeasure()测量


2.1 MeasureSpec

  • 首先要搞清楚,我们为什么要测量?
    因为在我们的xml布局文件中,我们设置width和height时会使用match_parent或warp_content来设置,测量的方法就是要将之变成具体的值,如250dp等。

  • MeasureSpec的基本知识
    每一个ViewGroup和View都会有MeasureSpec。MeasureSpec有32位字节组成,前两位:放三个模式,后30位:放控件的大小。
    每个子View的MeasureSpec:由父ViewGroup的MeasureSpec和子view的LayoutParams确定。在getChildMeasureSpec()方法中可以查看。
    MeasureSpec:exactly、at_most、unspecified
    LayoutParams: 100dp、match_parent、warp_content

以下是为每个子View确定MeasureSpec的方法代码:

如果父View的MeasureSpec模式是exactly,那么子View的是如精确值100dp,则子View的
MeasureSpec的模式是exactly,大小是100dp;子view的layoutparams是match_parent,
那么模式exactly,大小为父view的默认大小;子View是warp_content时,模式是at_most,大小为父view的默认大小。

如果父View的MeasureSpec模式是at_most,那么子View的是如精确值100dp,则子View的
MeasureSpec的模式是exactly,大小是100dp;子view的layoutparams是match_parent,
那么模式at_most,大小为父view的默认大小;子View是warp_content时,模式是at_most,大小为父view的默认大小。

如果父View的MeasureSpec模式是upspecified,那么子View的是如精确值100dp,则子View的
MeasureSpec的模式是exactly,大小是100dp;子view的layoutparams是match_parent,
那么模式upspecified,大小为父view的默认大小;子View是warp_content时,模式是upspecified,大小为父view的默认大小。

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
      int specMode = MeasureSpec.getMode(spec);
      int specSize = MeasureSpec.getSize(spec);

      int size = Math.max(0, specSize - padding);

      int resultSize = 0;
      int resultMode = 0;

      switch (specMode) {
      // Parent has imposed an exact size on us
      case MeasureSpec.EXACTLY:
          if (childDimension >= 0) {
              resultSize = childDimension;
              resultMode = MeasureSpec.EXACTLY;
          } else if (childDimension == LayoutParams.MATCH_PARENT) {
              // Child wants to be our size. So be it.
              resultSize = size;
              resultMode = MeasureSpec.EXACTLY;
          } else if (childDimension == LayoutParams.WRAP_CONTENT) {
              // Child wants to determine its own size. It can't be
              // bigger than us.
              resultSize = size;
              resultMode = MeasureSpec.AT_MOST;
          }
          break;

      // Parent has imposed a maximum size on us
      case MeasureSpec.AT_MOST:
          if (childDimension >= 0) {
              // Child wants a specific size... so be it
              resultSize = childDimension;
              resultMode = MeasureSpec.EXACTLY;
          } else if (childDimension == LayoutParams.MATCH_PARENT) {
              // Child wants to be our size, but our size is not fixed.
              // Constrain child to not be bigger than us.
              resultSize = size;
              resultMode = MeasureSpec.AT_MOST;
          } else if (childDimension == LayoutParams.WRAP_CONTENT) {
              // Child wants to determine its own size. It can't be
              // bigger than us.
              resultSize = size;
              resultMode = MeasureSpec.AT_MOST;
          }
          break;

      // Parent asked to see how big we want to be
      case MeasureSpec.UNSPECIFIED:
          if (childDimension >= 0) {
              // Child wants a specific size... let him have it
              resultSize = childDimension;
              resultMode = MeasureSpec.EXACTLY;
          } else if (childDimension == LayoutParams.MATCH_PARENT) {
              // Child wants to be our size... find out how big it should
              // be
              resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
              resultMode = MeasureSpec.UNSPECIFIED;
          } else if (childDimension == LayoutParams.WRAP_CONTENT) {
              // Child wants to determine its own size.... find out how
              // big it should be
              resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
              resultMode = MeasureSpec.UNSPECIFIED;
          }
          break;
      }
      //noinspection ResourceType
      return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
  }

2.2 onMeasure()测量的通用流程:
不同ViewGroup由于子View的显示布局样式不同,所以代码都可能不一样。但是它们在测量时会有一个基本的通用流程:

  1. 遍历子View为其设置MeasureSpec并调用子view的measure()方法:这一步通过getChildMeasureSpec()方法,也可以使用measureChildMargins()方法完成。
  2. 确定自定义view的大小:通过自身的measureSpec的模式和子view的所需大小,确定自定义view的大小。如viewGroup本身的模式是exactly则不需要理会子view所需的大小。这种模式关系来确定大小的关系可以自己定义,也可以有resolveSizeAndState()方法来确定。最后调用setMeasureDimension()来确定自定义view的大小。

onMeasure()通用流程的代码:

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
      //怎么测孩子呢?
      for (i in 0 until childCount) {
          val childView = getChildAt(i)
          //获得子View的LayoutParams来确定子View的measureSpec
          //可用替代measureChildWithMargins(childView,widthMeasureSpec,0,heightMeasureSpec,0)
          var childLP = childView.layoutParams
          //让ViewGroup的MeasureSpec和子View的LayoutParams,来确定子view的MeasureSpec
          val childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight, childLP.width)
          val childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, paddingTop + paddingBottom, childLP.height)
          childView.measure(childWidthMeasureSpec, childHeightMeasureSpec)
          }

      //自己定义不同模式的大小,也可以调用resolveSizeAndState()方法定义大小。
      val realWidth = if (widthMode == MeasureSpec.EXACTLY) selfWidth else parentNeedWidth
      val realHeight = if (heightMode == MeasureSpec.EXACTLY) selHeight else parentNeedHeight
      setMeasuredDimension(realWidth, realHeight)
}

2.3 举例瀑布流onMeasure()的测量:
布局分析:子View得换行:我们要先根据ViewGroup里MeasureSpec拿到默认的大小(一般都是上一个父View的最大值),然后跟子View所使用的宽度,如果比ViewGroup的默认大小要大,则换行。每一行的view都需要记录下来,以便在布局中确定位置。同时每一行的最大高度也需要记录下来,以便在布局中确定位置。同时也把每一行的最大宽度记录下来。下面是瀑布流布局的OnMeasure()方法代码

private val mVerticalSpacing = 0
  private val mHorizontalSpacing = 0
  private val allLines: MutableList<List<View?>> = ArrayList()
  private val lineHeights: MutableList<Int> = ArrayList()
  private var lineViews: MutableList<View> = ArrayList()

  fun clearList(){
      allLines.clear()
      lineHeights.clear()
      lineViews.clear()
  }

  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
      clearList()
      val childCount = childCount
      //拿到ViewGroup的默认的宽高
      val selfWidth = MeasureSpec.getSize(widthMeasureSpec)
      val selHeight = MeasureSpec.getSize(heightMeasureSpec)
      var parentNeedHeight = 0
      var parentNeedWidth = 0
      var lineWidthUsed = 0
      var lineHeight = 0 //一行的高度

      //怎么测孩子呢?
      for (i in 0 until childCount) {
          val childView = getChildAt(i)
          //获得子View的LayoutParams来确定子View的measureSpec
          //measureChildWithMargins(childView,widthMeasureSpec,0,heightMeasureSpec,0)
          var childLP = childView.layoutParams
          //让ViewGroup的MeasureSpec和子View的LayoutParams,来确定子view的MeasureSpec
          val childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight, childLP.width)
          val childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, paddingTop + paddingBottom, childLP.height)
          childView.measure(childWidthMeasureSpec, childHeightMeasureSpec)
          //获取子view的测量宽高
          val childMeasureWidth = childView.measuredWidth
          val childMeasureHeight = childView.measuredHeight
          //这个时候需要换行
          if (lineWidthUsed + childMeasureWidth > selfWidth) {
              allLines.add(lineViews)
              lineHeights.add(lineHeight)
              parentNeedWidth = Math.max(parentNeedWidth, lineWidthUsed) + mHorizontalSpacing
              parentNeedHeight = parentNeedHeight + lineHeight
              lineViews = ArrayList()
              lineWidthUsed = 0
              lineHeight = 0
          }
          //记录每一行的view
          lineViews.add(childView)
          lineWidthUsed = lineWidthUsed + childMeasureWidth  //每行已经添加的宽度值
          lineHeight = Math.max(lineHeight, childMeasureHeight)
          if (i == childCount - 1) {
              allLines.add(lineViews)
              lineHeights.add(lineHeight)
              parentNeedWidth = Math.max(parentNeedHeight, lineWidthUsed)
              parentNeedHeight = parentNeedHeight + lineHeight
          }

      }
      //再测量自己,确定自己得大小
      val widthMode = MeasureSpec.getMode(widthMeasureSpec)
      val heightMode = MeasureSpec.getMode(heightMeasureSpec)
      val realWidth = if (widthMode == MeasureSpec.EXACTLY) selfWidth else parentNeedWidth
      val realHeight = if (heightMode == MeasureSpec.EXACTLY) selHeight else parentNeedHeight
      setMeasuredDimension(realWidth, realHeight)
  }

3. onLayout()布局方法:

通过上面的onMeasure()方法,已经确定了ViewGroup的大小。这时通过调用子view的.layout(l,t,r,b)来确定每个子view的位置。


3.1 getLeft()、getX()、getRawX()的区别:
getLeft():是控件左边到手机屏幕坐标系的左边的位置。
getX():是手势点击的点,到所属控件的里面左边位置。
getRawX():是手势点击的点,到手机屏幕的左边位置。


3.2 getWidth()和getMeasureWidth()的区别
getWidth()和getHeight()是在onLayout()方法执行完才有效。
getMeasureWidth()和getMeasureHeight()在onMeasure()就有效。


3.3 例子瀑布流的onLayout()分析:
布局分析:在上面的onMeasure()方法中,我们已经记录了每行都有哪些view,因此我们只要计算每个子view的左上坐标,然后通过view.getMeasureWidth和view.getMeasureHeight的被测量过子view的宽高以此来确定四个点的位置。以下是代码。

override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
      //分为几行
      val lineCount = allLines.size
      var curL = paddingLeft
      var curT = paddingTop
      for (i in 0 until lineCount) {
          val lineView = allLines[i]
          val lineHeight = lineHeights[i]
          for (j in lineView.indices) {
              val view = lineView[j]
              val left = curL
              val top = curT
              val right = left + view!!.measuredWidth
              val bottom = top + view!!.measuredHeight
              view.layout(left, top, right, bottom)
              curL = right + mHorizontalSpacing
          }
          curT = curT + lineHeight + mVerticalSpacing
          curL = paddingLeft
      }
  }

写在最后:

这里写的一个FlowLayout只是用作学习自定义ViewGroup如何测量和布局的一个简单例子,由于时间关系只写出一个大概。不过现在我们要用到流布局一般会使用recyclerView+FlexboxLayoutManager来写既方便又快速,这里有篇文章可以参考:RecyclerView之使用FlexboxLayoutManager

相关文章

网友评论

    本文标题:View的绘制流程

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